(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 |
--------------------------------------------------------------------------------
/MAComponents/src/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 | }
--------------------------------------------------------------------------------
/MAComponents/src/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 |
--------------------------------------------------------------------------------
/MAComponents/src/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 |
--------------------------------------------------------------------------------
/MAComponents/src/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.general.Validate;
27 | import com.martinappl.components.ui.containers.interfaces.IViewObserver;
28 |
29 | /**
30 | *
31 | * @author Martin Appl
32 | *
33 | * Endless loop with items filling from adapter. Currently only horizontal orientation is implemented
34 | * View recycling in adapter is supported. You are encouraged to recycle view in adapter if possible
35 | *
36 | */
37 | public class EndlessLoopAdapterContainer extends AdapterView {
38 | /** Children added with this layout mode will be added after the last child */
39 | protected static final int LAYOUT_MODE_AFTER = 0;
40 |
41 | /** Children added with this layout mode will be added before the first child */
42 | protected static final int LAYOUT_MODE_TO_BEFORE = 1;
43 |
44 | protected static final int SCROLLING_DURATION = 500;
45 |
46 |
47 |
48 | /** The adapter providing data for container */
49 | protected Adapter mAdapter;
50 |
51 | /** The adaptor position of the first visible item */
52 | protected int mFirstItemPosition;
53 |
54 | /** The adaptor position of the last visible item */
55 | protected int mLastItemPosition;
56 |
57 | /** The adaptor position of selected item */
58 | protected int mSelectedPosition = INVALID_POSITION;
59 |
60 | /** Left of current most left child*/
61 | protected int mLeftChildEdge;
62 |
63 | /** User is not touching the list */
64 | protected static final int TOUCH_STATE_RESTING = 1;
65 |
66 | /** User is scrolling the list */
67 | protected static final int TOUCH_STATE_SCROLLING = 2;
68 |
69 | /** Fling gesture in progress */
70 | protected static final int TOUCH_STATE_FLING = 3;
71 |
72 | /** Aligning in progress */
73 | protected static final int TOUCH_STATE_ALIGN = 4;
74 |
75 | protected static final int TOUCH_STATE_DISTANCE_SCROLL = 5;
76 |
77 | /** A list of cached (re-usable) item views */
78 | protected final LinkedList> mCachedItemViews = new LinkedList>();
79 |
80 | /** 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*/
81 | protected boolean isSrollingDisabled = false;
82 |
83 | /** Whether content should be repeated when there is not enough items to fill container */
84 | protected boolean shouldRepeat = true;
85 |
86 | /** 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*/
87 | protected int mScrollPositionIfEndless = -1;
88 |
89 | private IViewObserver mViewObserver;
90 |
91 |
92 | protected int mTouchState = TOUCH_STATE_RESTING;
93 |
94 | protected final Scroller mScroller = new Scroller(getContext());
95 | private VelocityTracker mVelocityTracker;
96 | private boolean mDataChanged;
97 |
98 | private int mTouchSlop;
99 | private int mMinimumVelocity;
100 | private int mMaximumVelocity;
101 |
102 | private boolean mAllowLongPress;
103 | private float mLastMotionX;
104 | private float mLastMotionY;
105 | // private long mDownTime;
106 |
107 | private final Point mDown = new Point();
108 | private boolean mHandleSelectionOnActionUp = false;
109 | private boolean mInterceptTouchEvents;
110 | // private boolean mCancelInIntercept;
111 |
112 | protected OnItemClickListener mOnItemClickListener;
113 | protected OnItemSelectedListener mOnItemSelectedListener;
114 |
115 | public EndlessLoopAdapterContainer(Context context, AttributeSet attrs,
116 | int defStyle) {
117 | super(context, attrs, defStyle);
118 |
119 | final ViewConfiguration configuration = ViewConfiguration.get(context);
120 | mTouchSlop = configuration.getScaledTouchSlop();
121 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
122 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
123 |
124 | //init params from xml
125 | if(attrs != null){
126 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EndlessLoopAdapterContainer, defStyle, 0);
127 |
128 | shouldRepeat = a.getBoolean(R.styleable.EndlessLoopAdapterContainer_shouldRepeat, true);
129 |
130 | a.recycle();
131 | }
132 | }
133 |
134 | public EndlessLoopAdapterContainer(Context context, AttributeSet attrs) {
135 | this(context, attrs,0);
136 |
137 | }
138 |
139 | public EndlessLoopAdapterContainer(Context context) {
140 | this(context,null);
141 | }
142 |
143 | private final DataSetObserver fDataObserver = new DataSetObserver() {
144 |
145 | @Override
146 | public void onChanged() {
147 | synchronized(this){
148 | mDataChanged = true;
149 | }
150 | invalidate();
151 | }
152 |
153 | @Override
154 | public void onInvalidated() {
155 | mAdapter = null;
156 | }
157 | };
158 |
159 |
160 | /**
161 | * Params describing position of child view in container
162 | * in HORIZONTAL mode TOP,CENTER,BOTTOM are active in VERTICAL mode LEFT,CENTER,RIGHT are active
163 | * @author Martin Appl
164 | *
165 | */
166 | public static class LoopLayoutParams extends MarginLayoutParams{
167 | public static final int TOP = 0;
168 | public static final int CENTER = 1;
169 | public static final int BOTTOM = 2;
170 | public static final int LEFT = 3;
171 | public static final int RIGHT = 4;
172 |
173 | public int position;
174 | // public int actualWidth;
175 | // public int actualHeight;
176 |
177 | public LoopLayoutParams(int w, int h) {
178 | super(w, h);
179 | position = CENTER;
180 | }
181 |
182 | public LoopLayoutParams(int w, int h,int pos){
183 | super(w, h);
184 | position = pos;
185 | }
186 |
187 | public LoopLayoutParams(android.view.ViewGroup.LayoutParams lp) {
188 | super(lp);
189 |
190 | if(lp!=null && lp instanceof MarginLayoutParams){
191 | MarginLayoutParams mp = (MarginLayoutParams) lp;
192 | leftMargin = mp.leftMargin;
193 | rightMargin = mp.rightMargin;
194 | topMargin = mp.topMargin;
195 | bottomMargin = mp.bottomMargin;
196 | }
197 |
198 | position = CENTER;
199 | }
200 |
201 |
202 | }
203 |
204 | protected LoopLayoutParams createLayoutParams(int w, int h){
205 | return new LoopLayoutParams(w, h);
206 | }
207 |
208 | protected LoopLayoutParams createLayoutParams(int w, int h,int pos){
209 | return new LoopLayoutParams(w, h, pos);
210 | }
211 |
212 | protected LoopLayoutParams createLayoutParams(android.view.ViewGroup.LayoutParams lp){
213 | return new LoopLayoutParams(lp);
214 | }
215 |
216 |
217 | public boolean isRepeatable() {
218 | return shouldRepeat;
219 | }
220 |
221 | public boolean isEndlessRightNow(){
222 | return !isSrollingDisabled;
223 | }
224 |
225 | public void setShouldRepeat(boolean shouldRepeat) {
226 | this.shouldRepeat = shouldRepeat;
227 | }
228 |
229 | /**
230 | * Sets position in adapter of first shown item in container
231 | * @param position
232 | */
233 | public void scrollToPosition(int position){
234 | if(position < 0 || position >= mAdapter.getCount()) throw new IndexOutOfBoundsException("Position must be in bounds of adapter values count");
235 |
236 | reset();
237 | refillInternal(position-1, position);
238 | invalidate();
239 | }
240 |
241 | public void scrollToPositionIfEndless(int position){
242 | if(position < 0 || position >= mAdapter.getCount()) throw new IndexOutOfBoundsException("Position must be in bounds of adapter values count");
243 |
244 | if(isEndlessRightNow() && getChildCount() != 0){
245 | scrollToPosition(position);
246 | }
247 | else{
248 | mScrollPositionIfEndless = position;
249 | }
250 | }
251 |
252 | /**
253 | * Returns position to which will container scroll on next relayout
254 | * @return scroll position on next layout or -1 if it will scroll nowhere
255 | */
256 | public int getScrollPositionIfEndless(){
257 | return mScrollPositionIfEndless;
258 | }
259 |
260 | /**
261 | * Get index of currently first item in adapter
262 | * @return
263 | */
264 | public int getScrollPosition(){
265 | return mFirstItemPosition;
266 | }
267 |
268 | /**
269 | * Return offset by which is edge off first item moved off screen.
270 | * You can persist it and insert to setFirstItemOffset() to restore exact scroll position
271 | *
272 | * @return offset of first item, or 0 if there is not enough items to fill container and scrolling is disabled
273 | */
274 | public int getFirstItemOffset(){
275 | if(isSrollingDisabled) return 0;
276 | else return getScrollX() - mLeftChildEdge;
277 | }
278 |
279 | /**
280 | * Negative number. Offset by which is left edge of first item moved off screen.
281 | * @param offset
282 | */
283 | public void setFirstItemOffset(int offset){
284 | scrollTo(offset, 0);
285 | }
286 |
287 | @Override
288 | public Adapter getAdapter() {
289 | return mAdapter;
290 | }
291 |
292 | @Override
293 | public void setAdapter(Adapter adapter) {
294 | if(mAdapter != null) {
295 | mAdapter.unregisterDataSetObserver(fDataObserver);
296 | }
297 | mAdapter = adapter;
298 | mAdapter.registerDataSetObserver(fDataObserver);
299 |
300 | if(adapter instanceof IViewObserver){
301 | setViewObserver((IViewObserver) adapter);
302 | }
303 |
304 | reset();
305 | refill();
306 | invalidate();
307 | }
308 |
309 | @Override
310 | public View getSelectedView() {
311 | if(mSelectedPosition == INVALID_POSITION) return null;
312 |
313 | final int index;
314 | if(mFirstItemPosition > mSelectedPosition){
315 | index = mSelectedPosition + mAdapter.getCount() - mFirstItemPosition;
316 | }
317 | else{
318 | index = mSelectedPosition - mFirstItemPosition;
319 | }
320 | if(index < 0 || index >= getChildCount()) return null;
321 |
322 | return getChildAt(index);
323 | }
324 |
325 |
326 | /**
327 | * Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect
328 | */
329 | @Override
330 | public void setSelection(int position) {
331 | if(mAdapter == null) throw new IllegalStateException("You are trying to set selection on widget without adapter");
332 | if(mAdapter.getCount() == 0 && position == 0) position = -1;
333 | if(position < -1 || position > mAdapter.getCount()-1)
334 | throw new IllegalArgumentException("Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect");
335 |
336 | View v = getSelectedView();
337 | if(v != null) v.setSelected(false);
338 |
339 |
340 | final int oldPos = mSelectedPosition;
341 | mSelectedPosition = position;
342 |
343 | if(position == -1){
344 | if(mOnItemSelectedListener != null) mOnItemSelectedListener.onNothingSelected(this);
345 | return;
346 | }
347 |
348 | v = getSelectedView();
349 | if(v != null) v.setSelected(true);
350 |
351 | if(oldPos != mSelectedPosition && mOnItemSelectedListener != null) mOnItemSelectedListener.onItemSelected(this, v, mSelectedPosition, getSelectedItemId());
352 | }
353 |
354 |
355 | private void reset() {
356 | scrollTo(0, 0);
357 | removeAllViewsInLayout();
358 | mFirstItemPosition = 0;
359 | mLastItemPosition = -1;
360 | mLeftChildEdge = 0;
361 | }
362 |
363 |
364 | @Override
365 | public void computeScroll() {
366 | // if we don't have an adapter, we don't need to do anything
367 | if (mAdapter == null) {
368 | return;
369 | }
370 | if(mAdapter.getCount() == 0){
371 | return;
372 | }
373 |
374 | if (mScroller.computeScrollOffset()) {
375 | if(mScroller.getFinalX() == mScroller.getCurrX()){
376 | mScroller.abortAnimation();
377 | mTouchState = TOUCH_STATE_RESTING;
378 | if(!checkScrollPosition())
379 | clearChildrenCache();
380 | return;
381 | }
382 |
383 | int x = mScroller.getCurrX();
384 | scrollTo(x, 0);
385 |
386 | postInvalidate();
387 | }
388 | else if(mTouchState == TOUCH_STATE_FLING || mTouchState == TOUCH_STATE_DISTANCE_SCROLL){
389 | mTouchState = TOUCH_STATE_RESTING;
390 | if(!checkScrollPosition())
391 | clearChildrenCache();
392 | }
393 |
394 | if(mDataChanged){
395 | removeAllViewsInLayout();
396 | refillOnChange(mFirstItemPosition);
397 | return;
398 | }
399 |
400 | relayout();
401 | removeNonVisibleViews();
402 | refillRight();
403 | refillLeft();
404 |
405 | }
406 |
407 | /**
408 | *
409 | * @param velocityY The initial velocity in the Y direction. Positive
410 | * numbers mean that the finger/cursor is moving down the screen,
411 | * which means we want to scroll towards the top.
412 | * @param velocityX The initial velocity in the X direction. Positive
413 | * numbers mean that the finger/cursor is moving right the screen,
414 | * which means we want to scroll towards the top.
415 | */
416 | public void fling(int velocityX, int velocityY){
417 | mTouchState = TOUCH_STATE_FLING;
418 | final int x = getScrollX();
419 | final int y = getScrollY();
420 |
421 | mScroller.fling(x, y, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, Integer.MIN_VALUE,Integer.MAX_VALUE);
422 |
423 | invalidate();
424 | }
425 |
426 | /**
427 | * Scroll widget by given distance in pixels
428 | * @param dx
429 | */
430 | public void scroll(int dx){
431 | mScroller.startScroll(getScrollX(), 0, dx, 0, SCROLLING_DURATION);
432 | mTouchState = TOUCH_STATE_DISTANCE_SCROLL;
433 | invalidate();
434 | }
435 |
436 | @Override
437 | protected void onLayout(boolean changed, int left, int top, int right,
438 | int bottom) {
439 | super.onLayout(changed, left, top, right, bottom);
440 |
441 | // if we don't have an adapter, we don't need to do anything
442 | if (mAdapter == null) {
443 | return;
444 | }
445 |
446 | refillInternal(mLastItemPosition,mFirstItemPosition);
447 | }
448 |
449 | /**
450 | * Method for actualizing content after data change in adapter. It is expected container was emptied before
451 | * @param firstItemPosition
452 | */
453 | protected void refillOnChange(int firstItemPosition){
454 | refillInternal(firstItemPosition-1, firstItemPosition);
455 | }
456 |
457 |
458 | protected void refillInternal(final int lastItemPos,final int firstItemPos){
459 | // if we don't have an adapter, we don't need to do anything
460 | if (mAdapter == null) {
461 | return;
462 | }
463 | if(mAdapter.getCount() == 0){
464 | return;
465 | }
466 |
467 | if(getChildCount() == 0){
468 | fillFirstTime(lastItemPos, firstItemPos);
469 | }
470 | else{
471 | relayout();
472 | removeNonVisibleViews();
473 | refillRight();
474 | refillLeft();
475 | }
476 | }
477 |
478 | /**
479 | * Check if container visible area is filled and refill empty areas
480 | */
481 | private void refill(){
482 | scrollTo(0, 0);
483 | refillInternal(-1, 0);
484 | }
485 |
486 | // protected void measureChild(View child, LoopLayoutParams params){
487 | // //prepare spec for measurement
488 | // final int specW, specH;
489 | //
490 | // specW = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED), 0, params.width);
491 | // specH = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED), 0, params.height);
492 | //
493 | ////// final boolean useMeasuredW, useMeasuredH;
494 | //// if(params.height >= 0){
495 | //// specH = MeasureSpec.EXACTLY | params.height;
496 | ////// useMeasuredH = false;
497 | //// }
498 | //// else{
499 | //// if(params.height == LayoutParams.MATCH_PARENT){
500 | //// specH = MeasureSpec.EXACTLY | getHeight();
501 | //// params.height = getHeight();
502 | ////// useMeasuredH = false;
503 | //// }else{
504 | //// specH = MeasureSpec.AT_MOST | getHeight();
505 | ////// useMeasuredH = true;
506 | //// }
507 | //// }
508 | ////
509 | //// if(params.width >= 0){
510 | //// specW = MeasureSpec.EXACTLY | params.width;
511 | ////// useMeasuredW = false;
512 | //// }
513 | //// else{
514 | //// if(params.width == LayoutParams.MATCH_PARENT){
515 | //// specW = MeasureSpec.EXACTLY | getWidth();
516 | //// params.width = getWidth();
517 | ////// useMeasuredW = false;
518 | //// }else{
519 | //// specW = MeasureSpec.UNSPECIFIED;
520 | ////// useMeasuredW = true;
521 | //// }
522 | //// }
523 | //
524 | // //measure
525 | // child.measure(specW, specH);
526 | // //put measured values into layout params from where they will be used in layout.
527 | // //Use measured values only if exact values was not specified in layout params.
528 | //// if(useMeasuredH) params.actualHeight = child.getMeasuredHeight();
529 | //// else params.actualHeight = params.height;
530 | ////
531 | //// if(useMeasuredW) params.actualWidth = child.getMeasuredWidth();
532 | //// else params.actualWidth = params.width;
533 | // }
534 |
535 | protected void measureChild(View child){
536 | final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY);
537 | final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY);
538 | measureChild(child, pwms, phms);
539 | }
540 |
541 | private void relayout(){
542 | final int c = getChildCount();
543 | int left = mLeftChildEdge;
544 |
545 | View child;
546 | LoopLayoutParams lp;
547 | for(int i = 0; i < c; i++){
548 | child = getChildAt(i);
549 | lp = (LoopLayoutParams) child.getLayoutParams();
550 | measureChild(child);
551 |
552 | left = layoutChildHorizontal(child, left, lp);
553 | }
554 |
555 | }
556 |
557 |
558 | protected void fillFirstTime(final int lastItemPos,final int firstItemPos){
559 | final int leftScreenEdge = 0;
560 | final int rightScreenEdge = leftScreenEdge + getWidth();
561 |
562 | int right;
563 | int left;
564 | View child;
565 |
566 | boolean isRepeatingNow = false;
567 |
568 | //scrolling is enabled until we find out we don't have enough items
569 | isSrollingDisabled = false;
570 |
571 | mLastItemPosition = lastItemPos;
572 | mFirstItemPosition = firstItemPos;
573 | mLeftChildEdge = 0;
574 | right = mLeftChildEdge;
575 | left = mLeftChildEdge;
576 |
577 | while(right < rightScreenEdge){
578 | mLastItemPosition++;
579 |
580 | if(isRepeatingNow && mLastItemPosition >= firstItemPos) return;
581 |
582 | if(mLastItemPosition >= mAdapter.getCount()){
583 | if(firstItemPos == 0 && shouldRepeat) mLastItemPosition = 0;
584 | else{
585 | if(firstItemPos > 0){
586 | mLastItemPosition = 0;
587 | isRepeatingNow = true;
588 | }
589 | else if(!shouldRepeat){
590 | mLastItemPosition--;
591 | isSrollingDisabled = true;
592 | final int w = right-mLeftChildEdge;
593 | final int dx = (getWidth() - w)/2;
594 | scrollTo(-dx, 0);
595 | return;
596 | }
597 |
598 | }
599 | }
600 |
601 | if(mLastItemPosition >= mAdapter.getCount() ){
602 | Log.wtf("EndlessLoop", "mLastItemPosition > mAdapter.getCount()");
603 | return;
604 | }
605 |
606 | child = mAdapter.getView(mLastItemPosition, getCachedView(), this);
607 | Validate.notNull(child,"Your adapter has returned null from getView.");
608 | child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER);
609 | left = layoutChildHorizontal(child, left, (LoopLayoutParams) child.getLayoutParams());
610 | right = child.getRight();
611 |
612 | //if selected view is going to screen, set selected state on him
613 | if(mLastItemPosition == mSelectedPosition){
614 | child.setSelected(true);
615 | }
616 |
617 | }
618 |
619 | if(mScrollPositionIfEndless > 0){
620 | final int p = mScrollPositionIfEndless;
621 | mScrollPositionIfEndless = -1;
622 | removeAllViewsInLayout();
623 | refillOnChange(p);
624 | }
625 | }
626 |
627 |
628 | /**
629 | * Checks and refills empty area on the right
630 | */
631 | protected void refillRight(){
632 | if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch
633 | if(getChildCount() == 0) return;
634 |
635 | final int leftScreenEdge = getScrollX();
636 | final int rightScreenEdge = leftScreenEdge + getWidth();
637 |
638 | View child = getChildAt(getChildCount() - 1);
639 | int right = child.getRight();
640 | int currLayoutLeft = right + ((LoopLayoutParams)child.getLayoutParams()).rightMargin;
641 | while(right < rightScreenEdge){
642 | mLastItemPosition++;
643 | if(mLastItemPosition >= mAdapter.getCount()) mLastItemPosition = 0;
644 |
645 | child = mAdapter.getView(mLastItemPosition, getCachedView(), this);
646 | Validate.notNull(child,"Your adapter has returned null from getView.");
647 | child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER);
648 | currLayoutLeft = layoutChildHorizontal(child, currLayoutLeft, (LoopLayoutParams) child.getLayoutParams());
649 | right = child.getRight();
650 |
651 | //if selected view is going to screen, set selected state on him
652 | if(mLastItemPosition == mSelectedPosition){
653 | child.setSelected(true);
654 | }
655 | }
656 | }
657 |
658 | /**
659 | * Checks and refills empty area on the left
660 | */
661 | protected void refillLeft(){
662 | if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override first init to scrolling disabled by falling to this branch
663 | if(getChildCount() == 0) return;
664 |
665 | final int leftScreenEdge = getScrollX();
666 |
667 | View child = getChildAt(0);
668 | int childLeft = child.getLeft();
669 | int currLayoutRight = childLeft - ((LoopLayoutParams)child.getLayoutParams()).leftMargin;
670 | while(currLayoutRight > leftScreenEdge){
671 | mFirstItemPosition--;
672 | if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1;
673 |
674 | child = mAdapter.getView(mFirstItemPosition, getCachedView(), this);
675 | Validate.notNull(child,"Your adapter has returned null from getView.");
676 | child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE);
677 | currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams());
678 | childLeft = child.getLeft() - ((LoopLayoutParams)child.getLayoutParams()).leftMargin;
679 | //update left edge of children in container
680 | mLeftChildEdge = childLeft;
681 |
682 | //if selected view is going to screen, set selected state on him
683 | if(mFirstItemPosition == mSelectedPosition){
684 | child.setSelected(true);
685 | }
686 | }
687 | }
688 |
689 | // /**
690 | // * Checks and refills empty area on the left
691 | // */
692 | // protected void refillLeft(){
693 | // if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch
694 | // final int leftScreenEdge = getScrollX();
695 | //
696 | // View child = getChildAt(0);
697 | // int currLayoutRight = child.getRight();
698 | // while(currLayoutRight > leftScreenEdge){
699 | // mFirstItemPosition--;
700 | // if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1;
701 | //
702 | // child = mAdapter.getView(mFirstItemPosition, getCachedView(mFirstItemPosition), this);
703 | // child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE);
704 | // currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams());
705 | //
706 | // //update left edge of children in container
707 | // mLeftChildEdge = child.getLeft();
708 | //
709 | // //if selected view is going to screen, set selected state on him
710 | // if(mFirstItemPosition == mSelectedPosition){
711 | // child.setSelected(true);
712 | // }
713 | // }
714 | // }
715 |
716 | /**
717 | * Removes view that are outside of the visible part of the list. Will not
718 | * remove all views.
719 | */
720 | protected void removeNonVisibleViews() {
721 | if(getChildCount() == 0) return;
722 |
723 | final int leftScreenEdge = getScrollX();
724 | final int rightScreenEdge = leftScreenEdge + getWidth();
725 |
726 | // check if we should remove any views in the left
727 | View firstChild = getChildAt(0);
728 | final int leftedge = firstChild.getLeft() - ((LoopLayoutParams)firstChild.getLayoutParams()).leftMargin;
729 | if(leftedge != mLeftChildEdge) throw new IllegalStateException("firstChild.getLeft() != mLeftChildEdge");
730 | while (firstChild != null && firstChild.getRight() + ((LoopLayoutParams)firstChild.getLayoutParams()).rightMargin < leftScreenEdge) {
731 | //if selected view is going off screen, remove selected state
732 | firstChild.setSelected(false);
733 |
734 | // remove view
735 | removeViewInLayout(firstChild);
736 |
737 | if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(firstChild, mFirstItemPosition);
738 | WeakReference ref = new WeakReference(firstChild);
739 | mCachedItemViews.addLast(ref);
740 |
741 | mFirstItemPosition++;
742 | if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0;
743 |
744 | // update left item position
745 | mLeftChildEdge = getChildAt(0).getLeft() - ((LoopLayoutParams)getChildAt(0).getLayoutParams()).leftMargin;
746 |
747 | // Continue to check the next child only if we have more than
748 | // one child left
749 | if (getChildCount() > 1) {
750 | firstChild = getChildAt(0);
751 | } else {
752 | firstChild = null;
753 | }
754 | }
755 |
756 | // check if we should remove any views in the right
757 | View lastChild = getChildAt(getChildCount() - 1);
758 | while (lastChild != null && firstChild!=null && lastChild.getLeft() - ((LoopLayoutParams)firstChild.getLayoutParams()).leftMargin > rightScreenEdge) {
759 | //if selected view is going off screen, remove selected state
760 | lastChild.setSelected(false);
761 |
762 | // remove the right view
763 | removeViewInLayout(lastChild);
764 |
765 | if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(lastChild, mLastItemPosition);
766 | WeakReference ref = new WeakReference(lastChild);
767 | mCachedItemViews.addLast(ref);
768 |
769 | mLastItemPosition--;
770 | if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1;
771 |
772 | // Continue to check the next child only if we have more than
773 | // one child left
774 | if (getChildCount() > 1) {
775 | lastChild = getChildAt(getChildCount() - 1);
776 | } else {
777 | lastChild = null;
778 | }
779 | }
780 | }
781 |
782 |
783 | /**
784 | * Adds a view as a child view and takes care of measuring it
785 | *
786 | * @param child The view to add
787 | * @param layoutMode Either LAYOUT_MODE_LEFT or LAYOUT_MODE_RIGHT
788 | * @return child which was actually added to container, subclasses can override to introduce frame views
789 | */
790 | protected View addAndMeasureChildHorizontal(final View child, final int layoutMode) {
791 | LayoutParams lp = child.getLayoutParams();
792 | LoopLayoutParams params;
793 | if (lp == null) {
794 | params = createLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
795 | }
796 | else{
797 | if(lp!=null && lp instanceof LoopLayoutParams) params = (LoopLayoutParams) lp;
798 | else params = createLayoutParams(lp);
799 | }
800 | final int index = layoutMode == LAYOUT_MODE_TO_BEFORE ? 0 : -1;
801 | addViewInLayout(child, index, params, true);
802 |
803 | measureChild(child);
804 | child.setDrawingCacheEnabled(true);
805 |
806 | return child;
807 | }
808 |
809 |
810 |
811 | /**
812 | * Layouts children from left to right
813 | * @param left positon for left edge in parent container
814 | * @param lp layout params
815 | * @return new left
816 | */
817 | protected int layoutChildHorizontal(View v,int left, LoopLayoutParams lp){
818 | int l,t,r,b;
819 |
820 | switch(lp.position){
821 | case LoopLayoutParams.TOP:
822 | l = left + lp.leftMargin;
823 | t = lp.topMargin;
824 | r = l + v.getMeasuredWidth();
825 | b = t + v.getMeasuredHeight();
826 | break;
827 | case LoopLayoutParams.BOTTOM:
828 | b = getHeight() - lp.bottomMargin;
829 | t = b - v.getMeasuredHeight();
830 | l = left + lp.leftMargin;
831 | r = l + v.getMeasuredWidth();
832 | break;
833 | case LoopLayoutParams.CENTER:
834 | l = left + lp.leftMargin;
835 | r = l + v.getMeasuredWidth();
836 | final int x = (getHeight() - v.getMeasuredHeight())/2;
837 | t = x;
838 | b = t + v.getMeasuredHeight();
839 | break;
840 | default:
841 | throw new RuntimeException("Only TOP,BOTTOM,CENTER are alowed in horizontal orientation");
842 | }
843 |
844 |
845 | v.layout(l, t, r, b);
846 | return r + lp.rightMargin;
847 | }
848 |
849 | /**
850 | * Layout children from right to left
851 | */
852 | protected int layoutChildHorizontalToBefore(View v,int right , LoopLayoutParams lp){
853 | final int left = right - v.getMeasuredWidth() - lp.leftMargin - lp.rightMargin;
854 | layoutChildHorizontal(v, left, lp);
855 | return left;
856 | }
857 |
858 | /**
859 | * Allows to make scroll alignments
860 | * @return true if invalidate() was issued, and container is going to scroll
861 | */
862 | protected boolean checkScrollPosition(){
863 | return false;
864 | }
865 |
866 | @Override
867 | public boolean onInterceptTouchEvent(MotionEvent ev) {
868 |
869 | /*
870 | * This method JUST determines whether we want to intercept the motion.
871 | * If we return true, onTouchEvent will be called and we do the actual
872 | * scrolling there.
873 | */
874 |
875 |
876 | /*
877 | * Shortcut the most recurring case: the user is in the dragging
878 | * state and he is moving his finger. We want to intercept this
879 | * motion.
880 | */
881 | final int action = ev.getAction();
882 | if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) {
883 | return true;
884 | }
885 |
886 | final float x = ev.getX();
887 | final float y = ev.getY();
888 | switch (action) {
889 | case MotionEvent.ACTION_MOVE:
890 | //if we have scrolling disabled, we don't do anything
891 | if(!shouldRepeat && isSrollingDisabled) return false;
892 |
893 | /*
894 | * not dragging, otherwise the shortcut would have caught it. Check
895 | * whether the user has moved far enough from his original down touch.
896 | */
897 |
898 | /*
899 | * Locally do absolute value. mLastMotionX is set to the x value
900 | * of the down event.
901 | */
902 | final int xDiff = (int) Math.abs(x - mLastMotionX);
903 | final int yDiff = (int) Math.abs(y - mLastMotionY);
904 |
905 | final int touchSlop = mTouchSlop;
906 | final boolean xMoved = xDiff > touchSlop;
907 | final boolean yMoved = yDiff > touchSlop;
908 |
909 | if (xMoved) {
910 |
911 | // Scroll if the user moved far enough along the X axis
912 | mTouchState = TOUCH_STATE_SCROLLING;
913 | mHandleSelectionOnActionUp = false;
914 | enableChildrenCache();
915 |
916 | // Either way, cancel any pending longpress
917 | if (mAllowLongPress) {
918 | mAllowLongPress = false;
919 | // Try canceling the long press. It could also have been scheduled
920 | // by a distant descendant, so use the mAllowLongPress flag to block
921 | // everything
922 | cancelLongPress();
923 | }
924 | }
925 | if(yMoved){
926 | mHandleSelectionOnActionUp = false;
927 | if (mAllowLongPress) {
928 | mAllowLongPress = false;
929 | cancelLongPress();
930 | }
931 | }
932 | break;
933 |
934 | case MotionEvent.ACTION_DOWN:
935 | // Remember location of down touch
936 | mLastMotionX = x;
937 | mLastMotionY = y;
938 | mAllowLongPress = true;
939 | // mCancelInIntercept = false;
940 |
941 | mDown.x = (int) x;
942 | mDown.y = (int) y;
943 |
944 | /*
945 | * If being flinged and user touches the screen, initiate drag;
946 | * otherwise don't. mScroller.isFinished should be false when
947 | * being flinged.
948 | */
949 | mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING;
950 | //if he had normal click in rested state, remember for action up check
951 | if(mTouchState == TOUCH_STATE_RESTING){
952 | mHandleSelectionOnActionUp = true;
953 | }
954 | break;
955 |
956 | case MotionEvent.ACTION_CANCEL:
957 | mDown.x = -1;
958 | mDown.y = -1;
959 | // mCancelInIntercept = true;
960 | break;
961 | case MotionEvent.ACTION_UP:
962 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates
963 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){
964 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y);
965 | if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown);
966 | }
967 | // Release the drag
968 | mAllowLongPress = false;
969 | mHandleSelectionOnActionUp = false;
970 | mDown.x = -1;
971 | mDown.y = -1;
972 | if(mTouchState == TOUCH_STATE_SCROLLING){
973 | if(checkScrollPosition()){
974 | break;
975 | }
976 | }
977 | mTouchState = TOUCH_STATE_RESTING;
978 | clearChildrenCache();
979 | break;
980 | }
981 |
982 | mInterceptTouchEvents = mTouchState == TOUCH_STATE_SCROLLING;
983 | return mInterceptTouchEvents;
984 |
985 | }
986 |
987 | // /**
988 | // * Allow subclasses to override this to always intercept events
989 | // * @return
990 | // */
991 | // protected boolean interceptEvents(){
992 | // /*
993 | // * The only time we want to intercept motion events is if we are in the
994 | // * drag mode.
995 | // */
996 | // return mTouchState == TOUCH_STATE_SCROLLING;
997 | // }
998 |
999 | protected void handleClick(Point p){
1000 | final int c = getChildCount();
1001 | View v;
1002 | final Rect r = new Rect();
1003 | for(int i=0; i < c; i++){
1004 | v = getChildAt(i);
1005 | v.getHitRect(r);
1006 | if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){
1007 | final View old = getSelectedView();
1008 | if(old != null) old.setSelected(false);
1009 |
1010 | int position = mFirstItemPosition + i;
1011 | if(position >= mAdapter.getCount()) position = position - mAdapter.getCount();
1012 |
1013 |
1014 | mSelectedPosition = position;
1015 | v.setSelected(true);
1016 |
1017 | if(mOnItemClickListener != null) mOnItemClickListener.onItemClick(this, v, position , getItemIdAtPosition(position));
1018 | if(mOnItemSelectedListener != null) mOnItemSelectedListener.onItemSelected(this, v, position, getItemIdAtPosition(position));
1019 |
1020 | break;
1021 | }
1022 | }
1023 | }
1024 |
1025 |
1026 | @Override
1027 | public boolean onTouchEvent(MotionEvent event) {
1028 | // if we don't have an adapter, we don't need to do anything
1029 | if (mAdapter == null) {
1030 | return false;
1031 | }
1032 |
1033 |
1034 |
1035 | if (mVelocityTracker == null) {
1036 | mVelocityTracker = VelocityTracker.obtain();
1037 | }
1038 | mVelocityTracker.addMovement(event);
1039 |
1040 | final int action = event.getAction();
1041 | final float x = event.getX();
1042 | final float y = event.getY();
1043 |
1044 | switch (action) {
1045 | case MotionEvent.ACTION_DOWN:
1046 | /*
1047 | * If being flinged and user touches, stop the fling. isFinished
1048 | * will be false if being flinged.
1049 | */
1050 | if (!mScroller.isFinished()) {
1051 | mScroller.forceFinished(true);
1052 | }
1053 |
1054 | // Remember where the motion event started
1055 | mLastMotionX = x;
1056 | mLastMotionY = y;
1057 |
1058 | break;
1059 | case MotionEvent.ACTION_MOVE:
1060 | //if we have scrolling disabled, we don't do anything
1061 | if(!shouldRepeat && isSrollingDisabled) return false;
1062 |
1063 | if (mTouchState == TOUCH_STATE_SCROLLING) {
1064 | // Scroll to follow the motion event
1065 | final int deltaX = (int) (mLastMotionX - x);
1066 | mLastMotionX = x;
1067 | mLastMotionY = y;
1068 |
1069 | int sx = getScrollX() + deltaX;
1070 |
1071 | scrollTo(sx, 0);
1072 |
1073 | }
1074 | else{
1075 | final int xDiff = (int) Math.abs(x - mLastMotionX);
1076 |
1077 | final int touchSlop = mTouchSlop;
1078 | final boolean xMoved = xDiff > touchSlop;
1079 |
1080 |
1081 | if (xMoved) {
1082 |
1083 | // Scroll if the user moved far enough along the X axis
1084 | mTouchState = TOUCH_STATE_SCROLLING;
1085 | enableChildrenCache();
1086 |
1087 | // Either way, cancel any pending longpress
1088 | if (mAllowLongPress) {
1089 | mAllowLongPress = false;
1090 | // Try canceling the long press. It could also have been scheduled
1091 | // by a distant descendant, so use the mAllowLongPress flag to block
1092 | // everything
1093 | cancelLongPress();
1094 | }
1095 | }
1096 | }
1097 | break;
1098 | case MotionEvent.ACTION_UP:
1099 |
1100 | //this must be here, in case no child view returns true,
1101 | //events will propagate back here and on intercept touch event wont be called again
1102 | //in case of no parent it propagates here, in case of parent it usualy propagates to on cancel
1103 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){
1104 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y);
1105 | if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown);
1106 | mHandleSelectionOnActionUp = false;
1107 | }
1108 |
1109 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates
1110 | if (mTouchState == TOUCH_STATE_SCROLLING) {
1111 |
1112 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
1113 | int initialXVelocity = (int) mVelocityTracker.getXVelocity();
1114 | int initialYVelocity = (int) mVelocityTracker.getYVelocity();
1115 |
1116 | if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) {
1117 | fling(-initialXVelocity, -initialYVelocity);
1118 | }
1119 | else{
1120 | // Release the drag
1121 | clearChildrenCache();
1122 | mTouchState = TOUCH_STATE_RESTING;
1123 | checkScrollPosition();
1124 | mAllowLongPress = false;
1125 |
1126 | mDown.x = -1;
1127 | mDown.y = -1;
1128 | }
1129 |
1130 | if (mVelocityTracker != null) {
1131 | mVelocityTracker.recycle();
1132 | mVelocityTracker = null;
1133 | }
1134 |
1135 | break;
1136 | }
1137 |
1138 | // Release the drag
1139 | clearChildrenCache();
1140 | mTouchState = TOUCH_STATE_RESTING;
1141 | mAllowLongPress = false;
1142 |
1143 | mDown.x = -1;
1144 | mDown.y = -1;
1145 |
1146 | break;
1147 | case MotionEvent.ACTION_CANCEL:
1148 |
1149 | //this must be here, in case no child view returns true,
1150 | //events will propagate back here and on intercept touch event wont be called again
1151 | //instead we get cancel here, since we stated we shouldn't intercept events and propagate them to children
1152 | //but events propagated back here, because no child was interested
1153 | // if(!mInterceptTouchEvents && mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){
1154 | // handleClick(mDown);
1155 | // mHandleSelectionOnActionUp = false;
1156 | // }
1157 |
1158 | mAllowLongPress = false;
1159 |
1160 | mDown.x = -1;
1161 | mDown.y = -1;
1162 |
1163 | if(mTouchState == TOUCH_STATE_SCROLLING){
1164 | if(checkScrollPosition()){
1165 | break;
1166 | }
1167 | }
1168 |
1169 | mTouchState = TOUCH_STATE_RESTING;
1170 | }
1171 |
1172 | return true;
1173 | }
1174 |
1175 | @Override
1176 | public boolean onKeyDown(int keyCode, KeyEvent event) {
1177 | switch (keyCode) {
1178 | case KeyEvent.KEYCODE_DPAD_LEFT:
1179 | checkScrollFocusLeft();
1180 | break;
1181 | case KeyEvent.KEYCODE_DPAD_RIGHT:
1182 | checkScrollFocusRight();
1183 | break;
1184 | default:
1185 | break;
1186 | }
1187 |
1188 | return super.onKeyDown(keyCode, event);
1189 | }
1190 |
1191 | /**
1192 | * Moves with scroll window if focus hits one view before end of screen
1193 | */
1194 | private void checkScrollFocusLeft(){
1195 | final View focused = getFocusedChild();
1196 | if(getChildCount() >= 2 ){
1197 | View second = getChildAt(1);
1198 | View first = getChildAt(0);
1199 |
1200 | if(focused == second){
1201 | scroll(-first.getWidth());
1202 | }
1203 | }
1204 | }
1205 |
1206 | private void checkScrollFocusRight(){
1207 | final View focused = getFocusedChild();
1208 | if(getChildCount() >= 2 ){
1209 | View last = getChildAt(getChildCount()-1);
1210 | View lastButOne = getChildAt(getChildCount()-2);
1211 |
1212 | if(focused == lastButOne){
1213 | scroll(last.getWidth());
1214 | }
1215 | }
1216 | }
1217 |
1218 | /**
1219 | * Check if list of weak references has any view still in memory to offer for recyclation
1220 | * @return cached view
1221 | */
1222 | protected View getCachedView(){
1223 | if (mCachedItemViews.size() != 0) {
1224 | View v;
1225 | do{
1226 | v = mCachedItemViews.removeFirst().get();
1227 | }
1228 | while(v == null && mCachedItemViews.size() != 0);
1229 | return v;
1230 | }
1231 | return null;
1232 | }
1233 |
1234 | protected void enableChildrenCache() {
1235 | setChildrenDrawnWithCacheEnabled(true);
1236 | setChildrenDrawingCacheEnabled(true);
1237 | }
1238 |
1239 | protected void clearChildrenCache() {
1240 | setChildrenDrawnWithCacheEnabled(false);
1241 | }
1242 |
1243 | @Override
1244 | public void setOnItemClickListener(
1245 | android.widget.AdapterView.OnItemClickListener listener) {
1246 | mOnItemClickListener = listener;
1247 | }
1248 |
1249 | @Override
1250 | public void setOnItemSelectedListener(
1251 | android.widget.AdapterView.OnItemSelectedListener listener) {
1252 | mOnItemSelectedListener = listener;
1253 | }
1254 |
1255 | @Override
1256 | @CapturedViewProperty
1257 | public int getSelectedItemPosition() {
1258 | return mSelectedPosition;
1259 | }
1260 |
1261 | /**
1262 | * Only set value for selection position field, no gui updates are done
1263 | * for setting selection with gui updates and callback calls use setSelection
1264 | * @param position
1265 | */
1266 | public void setSeletedItemPosition(int position){
1267 | if(mAdapter.getCount() == 0 && position == 0) position = -1;
1268 | if(position < -1 || position > mAdapter.getCount()-1)
1269 | throw new IllegalArgumentException("Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect");
1270 |
1271 | mSelectedPosition = position;
1272 | }
1273 |
1274 | @Override
1275 | @CapturedViewProperty
1276 | public long getSelectedItemId() {
1277 | return mAdapter.getItemId(mSelectedPosition);
1278 | }
1279 |
1280 | @Override
1281 | public Object getSelectedItem() {
1282 | return getSelectedView();
1283 | }
1284 |
1285 | @Override
1286 | @CapturedViewProperty
1287 | public int getCount() {
1288 | if(mAdapter != null) return mAdapter.getCount();
1289 | else return 0;
1290 | }
1291 |
1292 | @Override
1293 | public int getPositionForView(View view) {
1294 | final int c = getChildCount();
1295 | View v;
1296 | for(int i = 0; i < c; i++){
1297 | v = getChildAt(i);
1298 | if(v == view) return mFirstItemPosition + i;
1299 | }
1300 | return INVALID_POSITION;
1301 | }
1302 |
1303 | @Override
1304 | public int getFirstVisiblePosition() {
1305 | return mFirstItemPosition;
1306 | }
1307 |
1308 | @Override
1309 | public int getLastVisiblePosition() {
1310 | return mLastItemPosition;
1311 | }
1312 |
1313 | @Override
1314 | public Object getItemAtPosition(int position) {
1315 | final int index;
1316 | if(mFirstItemPosition > position){
1317 | index = position + mAdapter.getCount() - mFirstItemPosition;
1318 | }
1319 | else{
1320 | index = position - mFirstItemPosition;
1321 | }
1322 | if(index < 0 || index >= getChildCount()) return null;
1323 |
1324 | return getChildAt(index);
1325 | }
1326 |
1327 | @Override
1328 | public long getItemIdAtPosition(int position) {
1329 | return mAdapter.getItemId(position);
1330 | }
1331 |
1332 | @Override
1333 | public boolean performItemClick(View view, int position, long id) {
1334 | throw new UnsupportedOperationException();
1335 | }
1336 |
1337 |
1338 | public void setViewObserver(IViewObserver viewObserver) {
1339 | this.mViewObserver = viewObserver;
1340 | }
1341 |
1342 |
1343 | }
1344 |
1345 |
1346 |
--------------------------------------------------------------------------------