├── .gitignore
├── HorizontalGridView
├── .classpath
├── .gitignore
├── .project
├── .settings
│ └── org.eclipse.jdt.core.prefs
├── AndroidManifest.xml
├── ic_launcher-web.png
├── libs
│ └── android-support-v4.jar
├── proguard-project.txt
├── project.properties
├── res
│ ├── drawable-hdpi
│ │ └── ic_launcher.png
│ ├── drawable-mdpi
│ │ └── ic_launcher.png
│ ├── drawable-xhdpi
│ │ └── ic_launcher.png
│ ├── drawable-xxhdpi
│ │ └── ic_launcher.png
│ ├── values-v11
│ │ └── styles.xml
│ ├── values-v14
│ │ └── styles.xml
│ └── values
│ │ ├── horizontal_gridview_attr.xml
│ │ ├── strings.xml
│ │ └── styles.xml
└── src
│ └── com
│ └── opensource
│ └── widget
│ ├── BaseGridView.java
│ ├── DefaultItemAnimator.java
│ ├── GridLayoutManager.java
│ ├── HorizontalGridView.java
│ ├── ItemAlignment.java
│ ├── LinearSmoothScroller.java
│ ├── OnChildSelectedListener.java
│ ├── RecyclerView.java
│ ├── StaggeredGrid.java
│ ├── StaggeredGridDefault.java
│ └── WindowAlignment.java
├── HorizontalGridView_Demo
├── .classpath
├── .gitignore
├── .project
├── .settings
│ └── org.eclipse.jdt.core.prefs
├── AndroidManifest.xml
├── ic_launcher-web.png
├── proguard-project.txt
├── project.properties
├── res
│ ├── drawable-hdpi
│ │ └── ic_launcher.png
│ ├── drawable-mdpi
│ │ └── ic_launcher.png
│ ├── drawable-xhdpi
│ │ └── ic_launcher.png
│ ├── drawable-xxhdpi
│ │ └── ic_launcher.png
│ ├── layout
│ │ └── activity_main.xml
│ ├── values-v11
│ │ └── styles.xml
│ └── values
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
└── src
│ └── com
│ └── opensource
│ └── widget
│ └── horizontalgridview
│ └── MainActivity.java
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea/workspace.xml
4 | .DS_Store
5 | /build
6 |
--------------------------------------------------------------------------------
/HorizontalGridView/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/HorizontalGridView/.gitignore:
--------------------------------------------------------------------------------
1 | /gen/
2 | /bin/
3 |
--------------------------------------------------------------------------------
/HorizontalGridView/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | HorizontalGridView
4 |
5 |
6 |
7 |
8 |
9 | com.android.ide.eclipse.adt.ResourceManagerBuilder
10 |
11 |
12 |
13 |
14 | com.android.ide.eclipse.adt.PreCompilerBuilder
15 |
16 |
17 |
18 |
19 | org.eclipse.jdt.core.javabuilder
20 |
21 |
22 |
23 |
24 | com.android.ide.eclipse.adt.ApkBuilder
25 |
26 |
27 |
28 |
29 |
30 | com.android.ide.eclipse.adt.AndroidNature
31 | org.eclipse.jdt.core.javanature
32 |
33 |
34 |
--------------------------------------------------------------------------------
/HorizontalGridView/.settings/org.eclipse.jdt.core.prefs:
--------------------------------------------------------------------------------
1 | eclipse.preferences.version=1
2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
3 | org.eclipse.jdt.core.compiler.compliance=1.6
4 | org.eclipse.jdt.core.compiler.source=1.6
5 |
--------------------------------------------------------------------------------
/HorizontalGridView/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
9 |
10 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/HorizontalGridView/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yinglovezhuzhu/HorizontalGridView/5322c3c0cbabe194a9af5b63285859bfc2b758d0/HorizontalGridView/ic_launcher-web.png
--------------------------------------------------------------------------------
/HorizontalGridView/libs/android-support-v4.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yinglovezhuzhu/HorizontalGridView/5322c3c0cbabe194a9af5b63285859bfc2b758d0/HorizontalGridView/libs/android-support-v4.jar
--------------------------------------------------------------------------------
/HorizontalGridView/proguard-project.txt:
--------------------------------------------------------------------------------
1 | # To enable ProGuard in your project, edit project.properties
2 | # to define the proguard.config property as described in that file.
3 | #
4 | # Add project specific ProGuard rules here.
5 | # By default, the flags in this file are appended to flags specified
6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt
7 | # You can edit the include path and order by changing the ProGuard
8 | # include property in project.properties.
9 | #
10 | # For more details, see
11 | # http://developer.android.com/guide/developing/tools/proguard.html
12 |
13 | # Add any project specific keep options here:
14 |
15 | # If your project uses WebView with JS, uncomment the following
16 | # and specify the fully qualified class name to the JavaScript interface
17 | # class:
18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
19 | # public *;
20 | #}
21 |
--------------------------------------------------------------------------------
/HorizontalGridView/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 |
13 | # Project target.
14 | target=android-19
15 | android.library=true
16 |
--------------------------------------------------------------------------------
/HorizontalGridView/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yinglovezhuzhu/HorizontalGridView/5322c3c0cbabe194a9af5b63285859bfc2b758d0/HorizontalGridView/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/HorizontalGridView/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yinglovezhuzhu/HorizontalGridView/5322c3c0cbabe194a9af5b63285859bfc2b758d0/HorizontalGridView/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/HorizontalGridView/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yinglovezhuzhu/HorizontalGridView/5322c3c0cbabe194a9af5b63285859bfc2b758d0/HorizontalGridView/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/HorizontalGridView/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yinglovezhuzhu/HorizontalGridView/5322c3c0cbabe194a9af5b63285859bfc2b758d0/HorizontalGridView/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/HorizontalGridView/res/values-v11/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/HorizontalGridView/res/values-v14/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/HorizontalGridView/res/values/horizontal_gridview_attr.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/HorizontalGridView/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | HorizontalGridView
4 |
5 |
6 |
--------------------------------------------------------------------------------
/HorizontalGridView/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
14 |
15 |
16 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/HorizontalGridView/src/com/opensource/widget/BaseGridView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project.
3 | *
4 | * yinglovezhuzhu@gmail.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package com.opensource.widget;
19 |
20 | import android.content.Context;
21 | import android.content.res.TypedArray;
22 | import android.graphics.Rect;
23 | import android.util.AttributeSet;
24 | import android.view.Gravity;
25 | import android.view.View;
26 |
27 | /**
28 | * Base class for vertically and horizontally scrolling lists. The items come
29 | * from the {@link com.opensource.widget.RecyclerView.Adapter} associated with this view.
30 | * @hide
31 | */
32 | abstract class BaseGridView extends RecyclerView {
33 |
34 | /**
35 | * Always keep focused item at a aligned position. Developer can use
36 | * WINDOW_ALIGN_XXX and ITEM_ALIGN_XXX to define how focused item is aligned.
37 | * In this mode, the last focused position will be remembered and restored when focus
38 | * is back to the view.
39 | */
40 | public final static int FOCUS_SCROLL_ALIGNED = 0;
41 |
42 | /**
43 | * Scroll to make the focused item inside client area.
44 | */
45 | public final static int FOCUS_SCROLL_ITEM = 1;
46 |
47 | /**
48 | * Scroll a page of items when focusing to item outside the client area.
49 | * The page size matches the client area size of RecyclerView.
50 | */
51 | public final static int FOCUS_SCROLL_PAGE = 2;
52 |
53 | /**
54 | * The first item is aligned with the low edge of the viewport. When
55 | * navigating away from the first item, the focus maintains a middle
56 | * location.
57 | *
58 | * The middle location is calculated by "windowAlignOffset" and
59 | * "windowAlignOffsetPercent"; if neither of these two is defined, the
60 | * default value is 1/2 of the size.
61 | */
62 | public final static int WINDOW_ALIGN_LOW_EDGE = 1;
63 |
64 | /**
65 | * The last item is aligned with the high edge of the viewport when
66 | * navigating to the end of list. When navigating away from the end, the
67 | * focus maintains a middle location.
68 | *
69 | * The middle location is calculated by "windowAlignOffset" and
70 | * "windowAlignOffsetPercent"; if neither of these two is defined, the
71 | * default value is 1/2 of the size.
72 | */
73 | public final static int WINDOW_ALIGN_HIGH_EDGE = 1 << 1;
74 |
75 | /**
76 | * The first item and last item are aligned with the two edges of the
77 | * viewport. When navigating in the middle of list, the focus maintains a
78 | * middle location.
79 | *
80 | * The middle location is calculated by "windowAlignOffset" and
81 | * "windowAlignOffsetPercent"; if neither of these two is defined, the
82 | * default value is 1/2 of the size.
83 | */
84 | public final static int WINDOW_ALIGN_BOTH_EDGE =
85 | WINDOW_ALIGN_LOW_EDGE | WINDOW_ALIGN_HIGH_EDGE;
86 |
87 | /**
88 | * The focused item always stays in a middle location.
89 | *
90 | * The middle location is calculated by "windowAlignOffset" and
91 | * "windowAlignOffsetPercent"; if neither of these two is defined, the
92 | * default value is 1/2 of the size.
93 | */
94 | public final static int WINDOW_ALIGN_NO_EDGE = 0;
95 |
96 | /**
97 | * Value indicates that percent is not used.
98 | */
99 | public final static float WINDOW_ALIGN_OFFSET_PERCENT_DISABLED = -1;
100 |
101 | /**
102 | * Value indicates that percent is not used.
103 | */
104 | public final static float ITEM_ALIGN_OFFSET_PERCENT_DISABLED = -1;
105 |
106 | protected final GridLayoutManager mLayoutManager;
107 |
108 | /**
109 | * Animate layout changes from a child resizing or adding/removing a child.
110 | */
111 | private boolean mAnimateChildLayout = true;
112 |
113 | private ItemAnimator mSavedItemAnimator;
114 |
115 | public BaseGridView(Context context, AttributeSet attrs, int defStyle) {
116 | super(context, attrs, defStyle);
117 | mLayoutManager = new GridLayoutManager(this);
118 | setLayoutManager(mLayoutManager);
119 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
120 | setHasFixedSize(true);
121 | setChildrenDrawingOrderEnabled(true);
122 | }
123 |
124 | protected void initBaseGridViewAttributes(Context context, AttributeSet attrs) {
125 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbBaseGridView);
126 | boolean throughFront = a.getBoolean(R.styleable.lbBaseGridView_focusOutFront, false);
127 | boolean throughEnd = a.getBoolean(R.styleable.lbBaseGridView_focusOutEnd, false);
128 | mLayoutManager.setFocusOutAllowed(throughFront, throughEnd);
129 | mLayoutManager.setVerticalMargin(
130 | a.getDimensionPixelSize(R.styleable.lbBaseGridView_verticalMargin, 0));
131 | mLayoutManager.setHorizontalMargin(
132 | a.getDimensionPixelSize(R.styleable.lbBaseGridView_horizontalMargin, 0));
133 | if (a.hasValue(R.styleable.lbBaseGridView_android_gravity)) {
134 | setGravity(a.getInt(R.styleable.lbBaseGridView_android_gravity, Gravity.NO_GRAVITY));
135 | }
136 | a.recycle();
137 | }
138 |
139 | /**
140 | * Set the strategy used to scroll in response to item focus changing:
141 | *
142 | * - {@link #FOCUS_SCROLL_ALIGNED} (default)
143 | * - {@link #FOCUS_SCROLL_ITEM}
144 | * - {@link #FOCUS_SCROLL_PAGE}
145 | *
146 | */
147 | public void setFocusScrollStrategy(int scrollStrategy) {
148 | if (scrollStrategy != FOCUS_SCROLL_ALIGNED && scrollStrategy != FOCUS_SCROLL_ITEM
149 | && scrollStrategy != FOCUS_SCROLL_PAGE) {
150 | throw new IllegalArgumentException("Invalid scrollStrategy");
151 | }
152 | mLayoutManager.setFocusScrollStrategy(scrollStrategy);
153 | requestLayout();
154 | }
155 |
156 | /**
157 | * Returns the strategy used to scroll in response to item focus changing.
158 | *
159 | * - {@link #FOCUS_SCROLL_ALIGNED} (default)
160 | * - {@link #FOCUS_SCROLL_ITEM}
161 | * - {@link #FOCUS_SCROLL_PAGE}
162 | *
163 | */
164 | public int getFocusScrollStrategy() {
165 | return mLayoutManager.getFocusScrollStrategy();
166 | }
167 |
168 | /**
169 | * Set how the focused item gets aligned in the view.
170 | *
171 | * @param windowAlignment {@link #WINDOW_ALIGN_BOTH_EDGE},
172 | * {@link #WINDOW_ALIGN_LOW_EDGE}, {@link #WINDOW_ALIGN_HIGH_EDGE} or
173 | * {@link #WINDOW_ALIGN_NO_EDGE}.
174 | */
175 | public void setWindowAlignment(int windowAlignment) {
176 | mLayoutManager.setWindowAlignment(windowAlignment);
177 | requestLayout();
178 | }
179 |
180 | /**
181 | * Get how the focused item gets aligned in the view.
182 | *
183 | * @return {@link #WINDOW_ALIGN_BOTH_EDGE}, {@link #WINDOW_ALIGN_LOW_EDGE},
184 | * {@link #WINDOW_ALIGN_HIGH_EDGE} or {@link #WINDOW_ALIGN_NO_EDGE}.
185 | */
186 | public int getWindowAlignment() {
187 | return mLayoutManager.getWindowAlignment();
188 | }
189 |
190 | /**
191 | * Set the absolute offset in pixels for window alignment.
192 | *
193 | * @param offset The number of pixels to offset. Can be negative for
194 | * alignment from the high edge, or positive for alignment from the
195 | * low edge.
196 | */
197 | public void setWindowAlignmentOffset(int offset) {
198 | mLayoutManager.setWindowAlignmentOffset(offset);
199 | requestLayout();
200 | }
201 |
202 | /**
203 | * Get the absolute offset in pixels for window alignment.
204 | *
205 | * @return The number of pixels to offset. Will be negative for alignment
206 | * from the high edge, or positive for alignment from the low edge.
207 | * Default value is 0.
208 | */
209 | public int getWindowAlignmentOffset() {
210 | return mLayoutManager.getWindowAlignmentOffset();
211 | }
212 |
213 | /**
214 | * Set offset percent for window alignment in addition to {@link
215 | * #getWindowAlignmentOffset()}.
216 | *
217 | * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
218 | * width from low edge. Use
219 | * {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
220 | */
221 | public void setWindowAlignmentOffsetPercent(float offsetPercent) {
222 | mLayoutManager.setWindowAlignmentOffsetPercent(offsetPercent);
223 | requestLayout();
224 | }
225 |
226 | /**
227 | * Get offset percent for window alignment in addition to
228 | * {@link #getWindowAlignmentOffset()}.
229 | *
230 | * @return Percentage to offset. E.g., 40 means 40% of the width from the
231 | * low edge, or {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} if
232 | * disabled. Default value is 50.
233 | */
234 | public float getWindowAlignmentOffsetPercent() {
235 | return mLayoutManager.getWindowAlignmentOffsetPercent();
236 | }
237 |
238 | /**
239 | * Set the absolute offset in pixels for item alignment.
240 | *
241 | * @param offset The number of pixels to offset. Can be negative for
242 | * alignment from the high edge, or positive for alignment from the
243 | * low edge.
244 | */
245 | public void setItemAlignmentOffset(int offset) {
246 | mLayoutManager.setItemAlignmentOffset(offset);
247 | requestLayout();
248 | }
249 |
250 | /**
251 | * Get the absolute offset in pixels for item alignment.
252 | *
253 | * @return The number of pixels to offset. Will be negative for alignment
254 | * from the high edge, or positive for alignment from the low edge.
255 | * Default value is 0.
256 | */
257 | public int getItemAlignmentOffset() {
258 | return mLayoutManager.getItemAlignmentOffset();
259 | }
260 |
261 | /**
262 | * Set to true if include padding in calculating item align offset.
263 | *
264 | * @param withPadding When it is true: we include left/top padding for positive
265 | * item offset, include right/bottom padding for negative item offset.
266 | */
267 | public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
268 | mLayoutManager.setItemAlignmentOffsetWithPadding(withPadding);
269 | requestLayout();
270 | }
271 |
272 | /**
273 | * Returns true if include padding in calculating item align offset.
274 | */
275 | public boolean isItemAlignmentOffsetWithPadding() {
276 | return mLayoutManager.isItemAlignmentOffsetWithPadding();
277 | }
278 |
279 | /**
280 | * Set offset percent for item alignment in addition to {@link
281 | * #getItemAlignmentOffset()}.
282 | *
283 | * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
284 | * width from the low edge. Use
285 | * {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
286 | */
287 | public void setItemAlignmentOffsetPercent(float offsetPercent) {
288 | mLayoutManager.setItemAlignmentOffsetPercent(offsetPercent);
289 | requestLayout();
290 | }
291 |
292 | /**
293 | * Get offset percent for item alignment in addition to {@link
294 | * #getItemAlignmentOffset()}.
295 | *
296 | * @return Percentage to offset. E.g., 40 means 40% of the width from the
297 | * low edge, or {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} if
298 | * disabled. Default value is 50.
299 | */
300 | public float getItemAlignmentOffsetPercent() {
301 | return mLayoutManager.getItemAlignmentOffsetPercent();
302 | }
303 |
304 | /**
305 | * Set the id of the view to align with. Use zero (default) for the item
306 | * view itself.
307 | */
308 | public void setItemAlignmentViewId(int viewId) {
309 | mLayoutManager.setItemAlignmentViewId(viewId);
310 | }
311 |
312 | /**
313 | * Get the id of the view to align with, or zero for the item view itself.
314 | */
315 | public int getItemAlignmentViewId() {
316 | return mLayoutManager.getItemAlignmentViewId();
317 | }
318 |
319 | /**
320 | * Set the margin in pixels between two child items.
321 | */
322 | public void setItemMargin(int margin) {
323 | mLayoutManager.setItemMargin(margin);
324 | requestLayout();
325 | }
326 |
327 | /**
328 | * Set the margin in pixels between two child items vertically.
329 | */
330 | public void setVerticalMargin(int margin) {
331 | mLayoutManager.setVerticalMargin(margin);
332 | requestLayout();
333 | }
334 |
335 | /**
336 | * Get the margin in pixels between two child items vertically.
337 | */
338 | public int getVerticalMargin() {
339 | return mLayoutManager.getVerticalMargin();
340 | }
341 |
342 | /**
343 | * Set the margin in pixels between two child items horizontally.
344 | */
345 | public void setHorizontalMargin(int margin) {
346 | mLayoutManager.setHorizontalMargin(margin);
347 | requestLayout();
348 | }
349 |
350 | /**
351 | * Get the margin in pixels between two child items horizontally.
352 | */
353 | public int getHorizontalMargin() {
354 | return mLayoutManager.getHorizontalMargin();
355 | }
356 |
357 | /**
358 | * Register a callback to be invoked when an item in BaseGridView has
359 | * been selected.
360 | */
361 | public void setOnChildSelectedListener(OnChildSelectedListener listener) {
362 | mLayoutManager.setOnChildSelectedListener(listener);
363 | }
364 |
365 | /**
366 | * Change the selected item immediately without animation.
367 | */
368 | public void setSelectedPosition(int position) {
369 | mLayoutManager.setSelection(this, position);
370 | }
371 |
372 | /**
373 | * Change the selected item and run an animation to scroll to the target
374 | * position.
375 | */
376 | public void setSelectedPositionSmooth(int position) {
377 | mLayoutManager.setSelectionSmooth(this, position);
378 | }
379 |
380 | /**
381 | * Get the selected item position.
382 | */
383 | public int getSelectedPosition() {
384 | return mLayoutManager.getSelection();
385 | }
386 |
387 | /**
388 | * Set if an animation should run when a child changes size or when adding
389 | * or removing a child.
390 | * Unstable API, might change later.
391 | */
392 | public void setAnimateChildLayout(boolean animateChildLayout) {
393 | if (mAnimateChildLayout != animateChildLayout) {
394 | mAnimateChildLayout = animateChildLayout;
395 | if (!mAnimateChildLayout) {
396 | mSavedItemAnimator = getItemAnimator();
397 | super.setItemAnimator(null);
398 | } else {
399 | super.setItemAnimator(mSavedItemAnimator);
400 | }
401 | }
402 | }
403 |
404 | /**
405 | * Return true if an animation will run when a child changes size or when
406 | * adding or removing a child.
407 | *
Unstable API, might change later.
408 | */
409 | public boolean isChildLayoutAnimated() {
410 | return mAnimateChildLayout;
411 | }
412 |
413 | /**
414 | * Describes how the child views are positioned. Defaults to
415 | * GRAVITY_TOP|GRAVITY_LEFT.
416 | *
417 | * @param gravity See {@link android.view.Gravity}
418 | */
419 | public void setGravity(int gravity) {
420 | mLayoutManager.setGravity(gravity);
421 | requestLayout();
422 | }
423 |
424 | @Override
425 | public void setDescendantFocusability (int focusability) {
426 | // enforce FOCUS_AFTER_DESCENDANTS
427 | super.setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
428 | }
429 |
430 | @Override
431 | public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
432 | return mLayoutManager.gridOnRequestFocusInDescendants(this, direction,
433 | previouslyFocusedRect);
434 | }
435 |
436 | /**
437 | * Get the x/y offsets to final position from current position if the view
438 | * is selected.
439 | *
440 | * @param view The view to get offsets.
441 | * @param offsets offsets[0] holds offset of X, offsets[1] holds offset of
442 | * Y.
443 | */
444 | public void getViewSelectedOffsets(View view, int[] offsets) {
445 | mLayoutManager.getViewSelectedOffsets(view, offsets);
446 | }
447 |
448 | @Override
449 | public int getChildDrawingOrder(int childCount, int i) {
450 | return mLayoutManager.getChildDrawingOrder(this, childCount, i);
451 | }
452 |
453 | final boolean isChildrenDrawingOrderEnabledInternal() {
454 | return isChildrenDrawingOrderEnabled();
455 | }
456 |
457 | /**
458 | * Disable or enable focus search.
459 | */
460 | public final void setFocusSearchDisabled(boolean disabled) {
461 | mLayoutManager.setFocusSearchDisabled(disabled);
462 | }
463 |
464 | /**
465 | * Return true if focus search is disabled.
466 | */
467 | public final boolean isFocusSearchDisabled() {
468 | return mLayoutManager.isFocusSearchDisabled();
469 | }
470 |
471 | /**
472 | * Enable or disable layout. All children will be removed when layout is
473 | * disabled.
474 | */
475 | public void setLayoutEnabled(boolean layoutEnabled) {
476 | mLayoutManager.setLayoutEnabled(layoutEnabled);
477 | }
478 |
479 | /**
480 | * Enable or disable pruning child. Disable is useful during transition.
481 | */
482 | public void setPruneChild(boolean pruneChild) {
483 | mLayoutManager.setPruneChild(pruneChild);
484 | }
485 |
486 | /**
487 | * Returns true if the view at the given position has a same row sibling
488 | * in front of it.
489 | *
490 | * @param position Position in adapter.
491 | */
492 | public boolean hasPreviousViewInSameRow(int position) {
493 | return mLayoutManager.hasPreviousViewInSameRow(position);
494 | }
495 | }
496 |
--------------------------------------------------------------------------------
/HorizontalGridView/src/com/opensource/widget/DefaultItemAnimator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project.
3 | *
4 | * yinglovezhuzhu@gmail.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package com.opensource.widget;
19 |
20 | import android.support.v4.view.ViewCompat;
21 | import android.support.v4.view.ViewPropertyAnimatorListener;
22 | import android.view.View;
23 |
24 | import com.opensource.widget.RecyclerView.ViewHolder;
25 |
26 | import java.util.ArrayList;
27 |
28 | /**
29 | * This implementation of {@link com.opensource.widget.RecyclerView.ItemAnimator} provides basic
30 | * animations on remove, add, and move events that happen to the items in
31 | * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
32 | *
33 | * @see com.opensource.widget.RecyclerView#setItemAnimator(com.opensource.widget.RecyclerView.ItemAnimator)
34 | */
35 | public class DefaultItemAnimator extends RecyclerView.ItemAnimator {
36 |
37 | private ArrayList mPendingRemovals = new ArrayList();
38 | private ArrayList mPendingAdditions = new ArrayList();
39 | private ArrayList mPendingMoves = new ArrayList();
40 |
41 | private ArrayList mAdditions = new ArrayList();
42 | private ArrayList mMoves = new ArrayList();
43 |
44 | private ArrayList mAddAnimations = new ArrayList();
45 | private ArrayList mMoveAnimations = new ArrayList();
46 | private ArrayList mRemoveAnimations = new ArrayList();
47 |
48 | private static class MoveInfo {
49 | public ViewHolder holder;
50 | public int fromX, fromY, toX, toY;
51 |
52 | private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
53 | this.holder = holder;
54 | this.fromX = fromX;
55 | this.fromY = fromY;
56 | this.toX = toX;
57 | this.toY = toY;
58 | }
59 | }
60 |
61 | @Override
62 | public void runPendingAnimations() {
63 | boolean removalsPending = !mPendingRemovals.isEmpty();
64 | boolean movesPending = !mPendingMoves.isEmpty();
65 | boolean additionsPending = !mPendingAdditions.isEmpty();
66 | if (!removalsPending && !movesPending && !additionsPending) {
67 | // nothing to animate
68 | return;
69 | }
70 | // First, remove stuff
71 | for (ViewHolder holder : mPendingRemovals) {
72 | animateRemoveImpl(holder);
73 | }
74 | mPendingRemovals.clear();
75 | // Next, move stuff
76 | if (movesPending) {
77 | mMoves.addAll(mPendingMoves);
78 | mPendingMoves.clear();
79 | Runnable mover = new Runnable() {
80 | @Override
81 | public void run() {
82 | for (MoveInfo moveInfo : mMoves) {
83 | animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
84 | moveInfo.toX, moveInfo.toY);
85 | }
86 | mMoves.clear();
87 | }
88 | };
89 | if (removalsPending) {
90 | View view = mMoves.get(0).holder.itemView;
91 | ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
92 | } else {
93 | mover.run();
94 | }
95 | }
96 | // Next, add stuff
97 | if (additionsPending) {
98 | mAdditions.addAll(mPendingAdditions);
99 | mPendingAdditions.clear();
100 | Runnable adder = new Runnable() {
101 | public void run() {
102 | for (ViewHolder holder : mAdditions) {
103 | animateAddImpl(holder);
104 | }
105 | mAdditions.clear();
106 | }
107 | };
108 | if (removalsPending || movesPending) {
109 | View view = mAdditions.get(0).itemView;
110 | ViewCompat.postOnAnimationDelayed(view, adder,
111 | (removalsPending ? getRemoveDuration() : 0) +
112 | (movesPending ? getMoveDuration() : 0));
113 | } else {
114 | adder.run();
115 | }
116 | }
117 | }
118 |
119 | @Override
120 | public boolean animateRemove(final ViewHolder holder) {
121 | mPendingRemovals.add(holder);
122 | return true;
123 | }
124 |
125 | private void animateRemoveImpl(final ViewHolder holder) {
126 | final View view = holder.itemView;
127 | ViewCompat.animate(view).cancel();
128 | ViewCompat.animate(view).setDuration(getRemoveDuration()).
129 | alpha(0).setListener(new VpaListenerAdapter() {
130 | @Override
131 | public void onAnimationEnd(View view) {
132 | ViewCompat.setAlpha(view, 1);
133 | dispatchRemoveFinished(holder);
134 | mRemoveAnimations.remove(holder);
135 | dispatchFinishedWhenDone();
136 | }
137 | }).start();
138 | mRemoveAnimations.add(holder);
139 | }
140 |
141 | @Override
142 | public boolean animateAdd(final ViewHolder holder) {
143 | ViewCompat.setAlpha(holder.itemView, 0);
144 | mPendingAdditions.add(holder);
145 | return true;
146 | }
147 |
148 | private void animateAddImpl(final ViewHolder holder) {
149 | final View view = holder.itemView;
150 | ViewCompat.animate(view).cancel();
151 | ViewCompat.animate(view).alpha(1).setDuration(getAddDuration()).
152 | setListener(new VpaListenerAdapter() {
153 | @Override
154 | public void onAnimationCancel(View view) {
155 | ViewCompat.setAlpha(view, 1);
156 | }
157 |
158 | @Override
159 | public void onAnimationEnd(View view) {
160 | dispatchAddFinished(holder);
161 | mAddAnimations.remove(holder);
162 | dispatchFinishedWhenDone();
163 | }
164 | }).start();
165 | mAddAnimations.add(holder);
166 | }
167 |
168 | @Override
169 | public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
170 | int toX, int toY) {
171 | final View view = holder.itemView;
172 | int deltaX = toX - fromX;
173 | int deltaY = toY - fromY;
174 | if (deltaX == 0 && deltaY == 0) {
175 | dispatchMoveFinished(holder);
176 | return false;
177 | }
178 | if (deltaX != 0) {
179 | ViewCompat.setTranslationX(view, -deltaX);
180 | }
181 | if (deltaY != 0) {
182 | ViewCompat.setTranslationY(view, -deltaY);
183 | }
184 | mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
185 | return true;
186 | }
187 |
188 | private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
189 | final View view = holder.itemView;
190 | final int deltaX = toX - fromX;
191 | final int deltaY = toY - fromY;
192 | ViewCompat.animate(view).cancel();
193 | if (deltaX != 0) {
194 | ViewCompat.animate(view).translationX(0);
195 | }
196 | if (deltaY != 0) {
197 | ViewCompat.animate(view).translationY(0);
198 | }
199 | // TODO: make EndActions end listeners instead, since end actions aren't called when
200 | // vpas are canceled (and can't end them. why?)
201 | // need listener functionality in VPACompat for this. Ick.
202 | ViewCompat.animate(view).setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
203 | @Override
204 | public void onAnimationCancel(View view) {
205 | if (deltaX != 0) {
206 | ViewCompat.setTranslationX(view, 0);
207 | }
208 | if (deltaY != 0) {
209 | ViewCompat.setTranslationY(view, 0);
210 | }
211 | }
212 | @Override
213 | public void onAnimationEnd(View view) {
214 | dispatchMoveFinished(holder);
215 | mMoveAnimations.remove(holder);
216 | dispatchFinishedWhenDone();
217 | }
218 | }).start();
219 | mMoveAnimations.add(holder);
220 | }
221 |
222 | @Override
223 | public void endAnimation(ViewHolder item) {
224 | final View view = item.itemView;
225 | ViewCompat.animate(view).cancel();
226 | if (mPendingMoves.contains(item)) {
227 | ViewCompat.setTranslationY(view, 0);
228 | ViewCompat.setTranslationX(view, 0);
229 | dispatchMoveFinished(item);
230 | mPendingMoves.remove(item);
231 | }
232 | if (mPendingRemovals.contains(item)) {
233 | dispatchRemoveFinished(item);
234 | mPendingRemovals.remove(item);
235 | }
236 | if (mPendingAdditions.contains(item)) {
237 | ViewCompat.setAlpha(view, 1);
238 | dispatchAddFinished(item);
239 | mPendingAdditions.remove(item);
240 | }
241 | if (mMoveAnimations.contains(item)) {
242 | ViewCompat.setTranslationY(view, 0);
243 | ViewCompat.setTranslationX(view, 0);
244 | dispatchMoveFinished(item);
245 | mMoveAnimations.remove(item);
246 | }
247 | if (mRemoveAnimations.contains(item)) {
248 | ViewCompat.setAlpha(view, 1);
249 | dispatchRemoveFinished(item);
250 | mRemoveAnimations.remove(item);
251 | }
252 | if (mAddAnimations.contains(item)) {
253 | ViewCompat.setAlpha(view, 1);
254 | dispatchAddFinished(item);
255 | mAddAnimations.remove(item);
256 | }
257 | dispatchFinishedWhenDone();
258 | }
259 |
260 | @Override
261 | public boolean isRunning() {
262 | return (!mMoveAnimations.isEmpty() ||
263 | !mRemoveAnimations.isEmpty() ||
264 | !mAddAnimations.isEmpty() ||
265 | !mMoves.isEmpty() ||
266 | !mAdditions.isEmpty());
267 | }
268 |
269 | /**
270 | * Check the state of currently pending and running animations. If there are none
271 | * pending/running, call {@link #dispatchAnimationsFinished()} to notify any
272 | * listeners.
273 | */
274 | private void dispatchFinishedWhenDone() {
275 | if (!isRunning()) {
276 | dispatchAnimationsFinished();
277 | }
278 | }
279 |
280 | @Override
281 | public void endAnimations() {
282 | int count = mPendingMoves.size();
283 | for (int i = count - 1; i >= 0; i--) {
284 | MoveInfo item = mPendingMoves.get(i);
285 | View view = item.holder.itemView;
286 | ViewCompat.animate(view).cancel();
287 | ViewCompat.setTranslationY(view, 0);
288 | ViewCompat.setTranslationX(view, 0);
289 | dispatchMoveFinished(item.holder);
290 | mPendingMoves.remove(item);
291 | }
292 | count = mPendingRemovals.size();
293 | for (int i = count - 1; i >= 0; i--) {
294 | ViewHolder item = mPendingRemovals.get(i);
295 | dispatchRemoveFinished(item);
296 | mPendingRemovals.remove(item);
297 | }
298 | count = mPendingAdditions.size();
299 | for (int i = count - 1; i >= 0; i--) {
300 | ViewHolder item = mPendingAdditions.get(i);
301 | View view = item.itemView;
302 | ViewCompat.setAlpha(view, 1);
303 | dispatchAddFinished(item);
304 | mPendingAdditions.remove(item);
305 | }
306 | if (!isRunning()) {
307 | return;
308 | }
309 | count = mMoveAnimations.size();
310 | for (int i = count - 1; i >= 0; i--) {
311 | ViewHolder item = mMoveAnimations.get(i);
312 | View view = item.itemView;
313 | ViewCompat.animate(view).cancel();
314 | ViewCompat.setTranslationY(view, 0);
315 | ViewCompat.setTranslationX(view, 0);
316 | dispatchMoveFinished(item);
317 | mMoveAnimations.remove(item);
318 | }
319 | count = mRemoveAnimations.size();
320 | for (int i = count - 1; i >= 0; i--) {
321 | ViewHolder item = mRemoveAnimations.get(i);
322 | View view = item.itemView;
323 | ViewCompat.animate(view).cancel();
324 | ViewCompat.setAlpha(view, 1);
325 | dispatchRemoveFinished(item);
326 | mRemoveAnimations.remove(item);
327 | }
328 | count = mAddAnimations.size();
329 | for (int i = count - 1; i >= 0; i--) {
330 | ViewHolder item = mAddAnimations.get(i);
331 | View view = item.itemView;
332 | ViewCompat.animate(view).cancel();
333 | ViewCompat.setAlpha(view, 1);
334 | dispatchAddFinished(item);
335 | mAddAnimations.remove(item);
336 | }
337 | mMoves.clear();
338 | mAdditions.clear();
339 | dispatchAnimationsFinished();
340 | }
341 |
342 | private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
343 | @Override
344 | public void onAnimationStart(View view) {}
345 |
346 | @Override
347 | public void onAnimationEnd(View view) {}
348 |
349 | @Override
350 | public void onAnimationCancel(View view) {}
351 | };
352 | }
353 |
--------------------------------------------------------------------------------
/HorizontalGridView/src/com/opensource/widget/GridLayoutManager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project.
3 | *
4 | * yinglovezhuzhu@gmail.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package com.opensource.widget;
19 |
20 | import android.content.Context;
21 | import android.graphics.PointF;
22 | import android.graphics.Rect;
23 | import android.util.AttributeSet;
24 | import android.util.Log;
25 | import android.view.FocusFinder;
26 | import android.view.Gravity;
27 | import android.view.View;
28 | import android.view.View.MeasureSpec;
29 | import android.view.ViewGroup;
30 | import android.view.ViewGroup.MarginLayoutParams;
31 | import android.view.ViewParent;
32 | import android.view.animation.DecelerateInterpolator;
33 | import android.view.animation.Interpolator;
34 |
35 | import com.opensource.widget.RecyclerView.Recycler;
36 | import com.opensource.widget.RecyclerView.State;
37 |
38 | import java.io.PrintWriter;
39 | import java.io.StringWriter;
40 | import java.util.ArrayList;
41 | import java.util.List;
42 |
43 | import static com.opensource.widget.RecyclerView.HORIZONTAL;
44 | import static com.opensource.widget.RecyclerView.NO_ID;
45 | import static com.opensource.widget.RecyclerView.NO_POSITION;
46 | import static com.opensource.widget.RecyclerView.VERTICAL;
47 |
48 | final class GridLayoutManager extends RecyclerView.LayoutManager {
49 |
50 | /*
51 | * LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}.
52 | * The class currently does two internal jobs:
53 | * - Saves optical bounds insets.
54 | * - Caches focus align view center.
55 | */
56 | static class LayoutParams extends RecyclerView.LayoutParams {
57 |
58 | // The view is saved only during animation.
59 | private View mView;
60 |
61 | // For placement
62 | private int mLeftInset;
63 | private int mTopInset;
64 | private int mRighInset;
65 | private int mBottomInset;
66 |
67 | // For alignment
68 | private int mAlignX;
69 | private int mAlignY;
70 |
71 | public LayoutParams(Context c, AttributeSet attrs) {
72 | super(c, attrs);
73 | }
74 |
75 | public LayoutParams(int width, int height) {
76 | super(width, height);
77 | }
78 |
79 | public LayoutParams(MarginLayoutParams source) {
80 | super(source);
81 | }
82 |
83 | public LayoutParams(ViewGroup.LayoutParams source) {
84 | super(source);
85 | }
86 |
87 | public LayoutParams(RecyclerView.LayoutParams source) {
88 | super(source);
89 | }
90 |
91 | public LayoutParams(LayoutParams source) {
92 | super(source);
93 | }
94 |
95 | int getAlignX() {
96 | return mAlignX;
97 | }
98 |
99 | int getAlignY() {
100 | return mAlignY;
101 | }
102 |
103 | int getOpticalLeft(View view) {
104 | return view.getLeft() + mLeftInset;
105 | }
106 |
107 | int getOpticalTop(View view) {
108 | return view.getTop() + mTopInset;
109 | }
110 |
111 | int getOpticalRight(View view) {
112 | return view.getRight() - mRighInset;
113 | }
114 |
115 | int getOpticalBottom(View view) {
116 | return view.getBottom() - mBottomInset;
117 | }
118 |
119 | int getOpticalWidth(View view) {
120 | return view.getWidth() - mLeftInset - mRighInset;
121 | }
122 |
123 | int getOpticalHeight(View view) {
124 | return view.getHeight() - mTopInset - mBottomInset;
125 | }
126 |
127 | int getOpticalLeftInset() {
128 | return mLeftInset;
129 | }
130 |
131 | int getOpticalRightInset() {
132 | return mRighInset;
133 | }
134 |
135 | int getOpticalTopInset() {
136 | return mTopInset;
137 | }
138 |
139 | int getOpticalBottomInset() {
140 | return mBottomInset;
141 | }
142 |
143 | void setAlignX(int alignX) {
144 | mAlignX = alignX;
145 | }
146 |
147 | void setAlignY(int alignY) {
148 | mAlignY = alignY;
149 | }
150 |
151 | void setOpticalInsets(int leftInset, int topInset, int rightInset, int bottomInset) {
152 | mLeftInset = leftInset;
153 | mTopInset = topInset;
154 | mRighInset = rightInset;
155 | mBottomInset = bottomInset;
156 | }
157 |
158 | private void invalidateItemDecoration() {
159 | ViewParent parent = mView.getParent();
160 | if (parent instanceof RecyclerView) {
161 | // TODO: we only need invalidate parent if it has ItemDecoration
162 | ((RecyclerView) parent).invalidate();
163 | }
164 | }
165 | }
166 |
167 | private static final String TAG = "GridLayoutManager";
168 | private static final boolean DEBUG = false;
169 |
170 | private static final Interpolator sDefaultAnimationChildLayoutInterpolator
171 | = new DecelerateInterpolator();
172 |
173 | private static final long DEFAULT_CHILD_ANIMATION_DURATION_MS = 250;
174 |
175 | private String getTag() {
176 | return TAG + ":" + mBaseGridView.getId();
177 | }
178 |
179 | private final BaseGridView mBaseGridView;
180 |
181 | /**
182 | * The orientation of a "row".
183 | */
184 | private int mOrientation = HORIZONTAL;
185 |
186 | private State mState;
187 | private Recycler mRecycler;
188 |
189 | private boolean mInLayout = false;
190 |
191 | private OnChildSelectedListener mChildSelectedListener = null;
192 |
193 | /**
194 | * The focused position, it's not the currently visually aligned position
195 | * but it is the final position that we intend to focus on. If there are
196 | * multiple setSelection() called, mFocusPosition saves last value.
197 | */
198 | private int mFocusPosition = NO_POSITION;
199 |
200 | /**
201 | * Force a full layout under certain situations.
202 | */
203 | private boolean mForceFullLayout;
204 |
205 | /**
206 | * True if layout is enabled.
207 | */
208 | private boolean mLayoutEnabled = true;
209 |
210 | /**
211 | * The scroll offsets of the viewport relative to the entire view.
212 | */
213 | private int mScrollOffsetPrimary;
214 | private int mScrollOffsetSecondary;
215 |
216 | /**
217 | * User-specified row height/column width. Can be WRAP_CONTENT.
218 | */
219 | private int mRowSizeSecondaryRequested;
220 |
221 | /**
222 | * The fixed size of each grid item in the secondary direction. This corresponds to
223 | * the row height, equal for all rows. Grid items may have variable length
224 | * in the primary direction.
225 | */
226 | private int mFixedRowSizeSecondary;
227 |
228 | /**
229 | * Tracks the secondary size of each row.
230 | */
231 | private int[] mRowSizeSecondary;
232 |
233 | /**
234 | * Flag controlling whether the current/next layout should
235 | * be updating the secondary size of rows.
236 | */
237 | private boolean mRowSecondarySizeRefresh;
238 |
239 | /**
240 | * The maximum measured size of the view.
241 | */
242 | private int mMaxSizeSecondary;
243 |
244 | /**
245 | * Margin between items.
246 | */
247 | private int mHorizontalMargin;
248 | /**
249 | * Margin between items vertically.
250 | */
251 | private int mVerticalMargin;
252 | /**
253 | * Margin in main direction.
254 | */
255 | private int mMarginPrimary;
256 | /**
257 | * Margin in second direction.
258 | */
259 | private int mMarginSecondary;
260 | /**
261 | * How to position child in secondary direction.
262 | */
263 | private int mGravity = Gravity.LEFT | Gravity.TOP;
264 | /**
265 | * The number of rows in the grid.
266 | */
267 | private int mNumRows;
268 | /**
269 | * Number of rows requested, can be 0 to be determined by parent size and
270 | * rowHeight.
271 | */
272 | private int mNumRowsRequested = 1;
273 |
274 | /**
275 | * Tracking start/end position of each row for visible items.
276 | */
277 | private StaggeredGrid.Row[] mRows;
278 |
279 | /**
280 | * Saves grid information of each view.
281 | */
282 | private StaggeredGrid mGrid;
283 | /**
284 | * Position of first item (included) that has attached views.
285 | */
286 | private int mFirstVisiblePos;
287 | /**
288 | * Position of last item (included) that has attached views.
289 | */
290 | private int mLastVisiblePos;
291 |
292 | /**
293 | * Focus Scroll strategy.
294 | */
295 | private int mFocusScrollStrategy = BaseGridView.FOCUS_SCROLL_ALIGNED;
296 | /**
297 | * Defines how item view is aligned in the window.
298 | */
299 | private final WindowAlignment mWindowAlignment = new WindowAlignment();
300 |
301 | /**
302 | * Defines how item view is aligned.
303 | */
304 | private final ItemAlignment mItemAlignment = new ItemAlignment();
305 |
306 | /**
307 | * Dimensions of the view, width or height depending on orientation.
308 | */
309 | private int mSizePrimary;
310 |
311 | /**
312 | * Allow DPAD key to navigate out at the front of the View (where position = 0),
313 | * default is false.
314 | */
315 | private boolean mFocusOutFront;
316 |
317 | /**
318 | * Allow DPAD key to navigate out at the end of the view, default is false.
319 | */
320 | private boolean mFocusOutEnd;
321 |
322 | /**
323 | * True if focus search is disabled.
324 | */
325 | private boolean mFocusSearchDisabled;
326 |
327 | /**
328 | * True if prune child, might be disabled during transition.
329 | */
330 | private boolean mPruneChild = true;
331 |
332 | private int[] mTempDeltas = new int[2];
333 |
334 | private boolean mUseDeltaInPreLayout;
335 |
336 | private int mDeltaInPreLayout, mDeltaSecondaryInPreLayout;
337 |
338 | /**
339 | * Temporaries used for measuring.
340 | */
341 | private int[] mMeasuredDimension = new int[2];
342 |
343 | public GridLayoutManager(BaseGridView baseGridView) {
344 | mBaseGridView = baseGridView;
345 | }
346 |
347 | public void setOrientation(int orientation) {
348 | if (orientation != HORIZONTAL && orientation != VERTICAL) {
349 | if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation);
350 | return;
351 | }
352 |
353 | mOrientation = orientation;
354 | mWindowAlignment.setOrientation(orientation);
355 | mItemAlignment.setOrientation(orientation);
356 | mForceFullLayout = true;
357 | }
358 |
359 | public int getFocusScrollStrategy() {
360 | return mFocusScrollStrategy;
361 | }
362 |
363 | public void setFocusScrollStrategy(int focusScrollStrategy) {
364 | mFocusScrollStrategy = focusScrollStrategy;
365 | }
366 |
367 | public void setWindowAlignment(int windowAlignment) {
368 | mWindowAlignment.mainAxis().setWindowAlignment(windowAlignment);
369 | }
370 |
371 | public int getWindowAlignment() {
372 | return mWindowAlignment.mainAxis().getWindowAlignment();
373 | }
374 |
375 | public void setWindowAlignmentOffset(int alignmentOffset) {
376 | mWindowAlignment.mainAxis().setWindowAlignmentOffset(alignmentOffset);
377 | }
378 |
379 | public int getWindowAlignmentOffset() {
380 | return mWindowAlignment.mainAxis().getWindowAlignmentOffset();
381 | }
382 |
383 | public void setWindowAlignmentOffsetPercent(float offsetPercent) {
384 | mWindowAlignment.mainAxis().setWindowAlignmentOffsetPercent(offsetPercent);
385 | }
386 |
387 | public float getWindowAlignmentOffsetPercent() {
388 | return mWindowAlignment.mainAxis().getWindowAlignmentOffsetPercent();
389 | }
390 |
391 | public void setItemAlignmentOffset(int alignmentOffset) {
392 | mItemAlignment.mainAxis().setItemAlignmentOffset(alignmentOffset);
393 | updateChildAlignments();
394 | }
395 |
396 | public int getItemAlignmentOffset() {
397 | return mItemAlignment.mainAxis().getItemAlignmentOffset();
398 | }
399 |
400 | public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
401 | mItemAlignment.mainAxis().setItemAlignmentOffsetWithPadding(withPadding);
402 | updateChildAlignments();
403 | }
404 |
405 | public boolean isItemAlignmentOffsetWithPadding() {
406 | return mItemAlignment.mainAxis().isItemAlignmentOffsetWithPadding();
407 | }
408 |
409 | public void setItemAlignmentOffsetPercent(float offsetPercent) {
410 | mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent);
411 | updateChildAlignments();
412 | }
413 |
414 | public float getItemAlignmentOffsetPercent() {
415 | return mItemAlignment.mainAxis().getItemAlignmentOffsetPercent();
416 | }
417 |
418 | public void setItemAlignmentViewId(int viewId) {
419 | mItemAlignment.mainAxis().setItemAlignmentViewId(viewId);
420 | updateChildAlignments();
421 | }
422 |
423 | public int getItemAlignmentViewId() {
424 | return mItemAlignment.mainAxis().getItemAlignmentViewId();
425 | }
426 |
427 | public void setFocusOutAllowed(boolean throughFront, boolean throughEnd) {
428 | mFocusOutFront = throughFront;
429 | mFocusOutEnd = throughEnd;
430 | }
431 |
432 | public void setNumRows(int numRows) {
433 | if (numRows < 0) throw new IllegalArgumentException();
434 | mNumRowsRequested = numRows;
435 | mForceFullLayout = true;
436 | }
437 |
438 | /**
439 | * Set the row height. May be WRAP_CONTENT, or a size in pixels.
440 | */
441 | public void setRowHeight(int height) {
442 | if (height >= 0 || height == ViewGroup.LayoutParams.WRAP_CONTENT) {
443 | mRowSizeSecondaryRequested = height;
444 | } else {
445 | throw new IllegalArgumentException("Invalid row height: " + height);
446 | }
447 | }
448 |
449 | public void setItemMargin(int margin) {
450 | mVerticalMargin = mHorizontalMargin = margin;
451 | mMarginPrimary = mMarginSecondary = margin;
452 | }
453 |
454 | public void setVerticalMargin(int margin) {
455 | if (mOrientation == HORIZONTAL) {
456 | mMarginSecondary = mVerticalMargin = margin;
457 | } else {
458 | mMarginPrimary = mVerticalMargin = margin;
459 | }
460 | }
461 |
462 | public void setHorizontalMargin(int margin) {
463 | if (mOrientation == HORIZONTAL) {
464 | mMarginPrimary = mHorizontalMargin = margin;
465 | } else {
466 | mMarginSecondary = mHorizontalMargin = margin;
467 | }
468 | }
469 |
470 | public int getVerticalMargin() {
471 | return mVerticalMargin;
472 | }
473 |
474 | public int getHorizontalMargin() {
475 | return mHorizontalMargin;
476 | }
477 |
478 | public void setGravity(int gravity) {
479 | mGravity = gravity;
480 | }
481 |
482 | protected boolean hasDoneFirstLayout() {
483 | return mGrid != null;
484 | }
485 |
486 | public void setOnChildSelectedListener(OnChildSelectedListener listener) {
487 | mChildSelectedListener = listener;
488 | }
489 |
490 | private int getPositionByView(View view) {
491 | if (view == null) {
492 | return NO_POSITION;
493 | }
494 | RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
495 | if (vh == null) {
496 | return NO_POSITION;
497 | }
498 | return vh.getPosition();
499 | }
500 |
501 | private int getPositionByIndex(int index) {
502 | return getPositionByView(getChildAt(index));
503 | }
504 |
505 | private void dispatchChildSelected() {
506 | if (mChildSelectedListener == null) {
507 | return;
508 | }
509 | if (mFocusPosition != NO_POSITION) {
510 | View view = findViewByPosition(mFocusPosition);
511 | if (view != null) {
512 | RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
513 | mChildSelectedListener.onChildSelected(mBaseGridView, view, mFocusPosition,
514 | vh == null? NO_ID: vh.getItemId());
515 | return;
516 | }
517 | }
518 | mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID);
519 | }
520 |
521 | @Override
522 | public boolean canScrollHorizontally() {
523 | // We can scroll horizontally if we have horizontal orientation, or if
524 | // we are vertical and have more than one column.
525 | return mOrientation == HORIZONTAL || mNumRows > 1;
526 | }
527 |
528 | @Override
529 | public boolean canScrollVertically() {
530 | // We can scroll vertically if we have vertical orientation, or if we
531 | // are horizontal and have more than one row.
532 | return mOrientation == VERTICAL || mNumRows > 1;
533 | }
534 |
535 | @Override
536 | public RecyclerView.LayoutParams generateDefaultLayoutParams() {
537 | return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
538 | ViewGroup.LayoutParams.WRAP_CONTENT);
539 | }
540 |
541 | @Override
542 | public RecyclerView.LayoutParams generateLayoutParams(Context context, AttributeSet attrs) {
543 | return new LayoutParams(context, attrs);
544 | }
545 |
546 | @Override
547 | public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
548 | if (lp instanceof LayoutParams) {
549 | return new LayoutParams((LayoutParams) lp);
550 | } else if (lp instanceof RecyclerView.LayoutParams) {
551 | return new LayoutParams((RecyclerView.LayoutParams) lp);
552 | } else if (lp instanceof MarginLayoutParams) {
553 | return new LayoutParams((MarginLayoutParams) lp);
554 | } else {
555 | return new LayoutParams(lp);
556 | }
557 | }
558 |
559 | protected View getViewForPosition(int position) {
560 | return mRecycler.getViewForPosition(position);
561 | }
562 |
563 | final int getOpticalLeft(View v) {
564 | return ((LayoutParams) v.getLayoutParams()).getOpticalLeft(v);
565 | }
566 |
567 | final int getOpticalRight(View v) {
568 | return ((LayoutParams) v.getLayoutParams()).getOpticalRight(v);
569 | }
570 |
571 | final int getOpticalTop(View v) {
572 | return ((LayoutParams) v.getLayoutParams()).getOpticalTop(v);
573 | }
574 |
575 | final int getOpticalBottom(View v) {
576 | return ((LayoutParams) v.getLayoutParams()).getOpticalBottom(v);
577 | }
578 |
579 | private int getViewMin(View v) {
580 | return (mOrientation == HORIZONTAL) ? getOpticalLeft(v) : getOpticalTop(v);
581 | }
582 |
583 | private int getViewMax(View v) {
584 | return (mOrientation == HORIZONTAL) ? getOpticalRight(v) : getOpticalBottom(v);
585 | }
586 |
587 | private int getViewCenter(View view) {
588 | return (mOrientation == HORIZONTAL) ? getViewCenterX(view) : getViewCenterY(view);
589 | }
590 |
591 | private int getViewCenterSecondary(View view) {
592 | return (mOrientation == HORIZONTAL) ? getViewCenterY(view) : getViewCenterX(view);
593 | }
594 |
595 | private int getViewCenterX(View v) {
596 | LayoutParams p = (LayoutParams) v.getLayoutParams();
597 | return p.getOpticalLeft(v) + p.getAlignX();
598 | }
599 |
600 | private int getViewCenterY(View v) {
601 | LayoutParams p = (LayoutParams) v.getLayoutParams();
602 | return p.getOpticalTop(v) + p.getAlignY();
603 | }
604 |
605 | /**
606 | * Save Recycler and State for convenience. Must be paired with leaveContext().
607 | */
608 | private void saveContext(Recycler recycler, State state) {
609 | if (mRecycler != null || mState != null) {
610 | Log.e(TAG, "Recycler information was not released, bug!");
611 | }
612 | mRecycler = recycler;
613 | mState = state;
614 | }
615 |
616 | /**
617 | * Discard saved Recycler and State.
618 | */
619 | private void leaveContext() {
620 | mRecycler = null;
621 | mState = null;
622 | }
623 |
624 | /**
625 | * Re-initialize data structures for a data change or handling invisible
626 | * selection. The method tries its best to preserve position information so
627 | * that staggered grid looks same before and after re-initialize.
628 | * @param focusPosition The initial focusPosition that we would like to
629 | * focus on.
630 | * @return Actual position that can be focused on.
631 | */
632 | private int init(int focusPosition) {
633 |
634 | final int newItemCount = mState.getItemCount();
635 |
636 | if (focusPosition == NO_POSITION && newItemCount > 0) {
637 | // if focus position is never set before, initialize it to 0
638 | focusPosition = 0;
639 | }
640 | // If adapter has changed then caches are invalid; otherwise,
641 | // we try to maintain each row's position if number of rows keeps the same
642 | // and existing mGrid contains the focusPosition.
643 | if (mRows != null && mNumRows == mRows.length &&
644 | mGrid != null && mGrid.getSize() > 0 && focusPosition >= 0 &&
645 | focusPosition >= mGrid.getFirstIndex() &&
646 | focusPosition <= mGrid.getLastIndex()) {
647 | // strip mGrid to a subset (like a column) that contains focusPosition
648 | mGrid.stripDownTo(focusPosition);
649 | // make sure that remaining items do not exceed new adapter size
650 | int firstIndex = mGrid.getFirstIndex();
651 | int lastIndex = mGrid.getLastIndex();
652 | if (DEBUG) {
653 | Log.v(getTag(), "mGrid firstIndex " + firstIndex + " lastIndex " + lastIndex);
654 | }
655 | for (int i = lastIndex; i >=firstIndex; i--) {
656 | if (i >= newItemCount) {
657 | mGrid.removeLast();
658 | }
659 | }
660 | if (mGrid.getSize() == 0) {
661 | focusPosition = newItemCount - 1;
662 | // initialize row start locations
663 | for (int i = 0; i < mNumRows; i++) {
664 | mRows[i].low = 0;
665 | mRows[i].high = 0;
666 | }
667 | if (DEBUG) Log.v(getTag(), "mGrid zero size");
668 | } else {
669 | // initialize row start locations
670 | for (int i = 0; i < mNumRows; i++) {
671 | mRows[i].low = Integer.MAX_VALUE;
672 | mRows[i].high = Integer.MIN_VALUE;
673 | }
674 | firstIndex = mGrid.getFirstIndex();
675 | lastIndex = mGrid.getLastIndex();
676 | if (focusPosition > lastIndex) {
677 | focusPosition = mGrid.getLastIndex();
678 | }
679 | if (DEBUG) {
680 | Log.v(getTag(), "mGrid firstIndex " + firstIndex + " lastIndex "
681 | + lastIndex + " focusPosition " + focusPosition);
682 | }
683 | // fill rows with minimal view positions of the subset
684 | for (int i = firstIndex; i <= lastIndex; i++) {
685 | View v = findViewByPosition(i);
686 | if (v == null) {
687 | continue;
688 | }
689 | int row = mGrid.getLocation(i).row;
690 | int low = getViewMin(v) + mScrollOffsetPrimary;
691 | if (low < mRows[row].low) {
692 | mRows[row].low = mRows[row].high = low;
693 | }
694 | }
695 | int firstItemRowPosition = mRows[mGrid.getLocation(firstIndex).row].low;
696 | if (firstItemRowPosition == Integer.MAX_VALUE) {
697 | firstItemRowPosition = 0;
698 | }
699 | if (mState.didStructureChange()) {
700 | // if there is structure change, the removed item might be in the
701 | // subset, so it is meaningless to maintain the low locations.
702 | for (int i = 0; i < mNumRows; i++) {
703 | mRows[i].low = firstItemRowPosition;
704 | mRows[i].high = firstItemRowPosition;
705 | }
706 | } else {
707 | // fill other rows that does not include the subset using first item
708 | for (int i = 0; i < mNumRows; i++) {
709 | if (mRows[i].low == Integer.MAX_VALUE) {
710 | mRows[i].low = mRows[i].high = firstItemRowPosition;
711 | }
712 | }
713 | }
714 | }
715 |
716 | // Same adapter, we can reuse any attached views
717 | detachAndScrapAttachedViews(mRecycler);
718 |
719 | } else {
720 | // otherwise recreate data structure
721 | mRows = new StaggeredGrid.Row[mNumRows];
722 |
723 | for (int i = 0; i < mNumRows; i++) {
724 | mRows[i] = new StaggeredGrid.Row();
725 | }
726 | mGrid = new StaggeredGridDefault();
727 | if (newItemCount == 0) {
728 | focusPosition = NO_POSITION;
729 | } else if (focusPosition >= newItemCount) {
730 | focusPosition = newItemCount - 1;
731 | }
732 |
733 | // Adapter may have changed so remove all attached views permanently
734 | removeAndRecycleAllViews(mRecycler);
735 |
736 | mScrollOffsetPrimary = 0;
737 | mScrollOffsetSecondary = 0;
738 | mWindowAlignment.reset();
739 | }
740 |
741 | mGrid.setProvider(mGridProvider);
742 | // mGrid share the same Row array information
743 | mGrid.setRows(mRows);
744 | mFirstVisiblePos = mLastVisiblePos = NO_POSITION;
745 |
746 | initScrollController();
747 | updateScrollSecondAxis();
748 |
749 | return focusPosition;
750 | }
751 |
752 | private int getRowSizeSecondary(int rowIndex) {
753 | if (mFixedRowSizeSecondary != 0) {
754 | return mFixedRowSizeSecondary;
755 | }
756 | if (mRowSizeSecondary == null) {
757 | return 0;
758 | }
759 | return mRowSizeSecondary[rowIndex];
760 | }
761 |
762 | private int getRowStartSecondary(int rowIndex) {
763 | int start = 0;
764 | for (int i = 0; i < rowIndex; i++) {
765 | start += getRowSizeSecondary(i) + mMarginSecondary;
766 | }
767 | return start;
768 | }
769 |
770 | private int getSizeSecondary() {
771 | return getRowStartSecondary(mNumRows - 1) + getRowSizeSecondary(mNumRows - 1);
772 | }
773 |
774 | private void measureScrapChild(int position, int widthSpec, int heightSpec,
775 | int[] measuredDimension) {
776 | View view = mRecycler.getViewForPosition(position);
777 | if (view != null) {
778 | LayoutParams p = (LayoutParams) view.getLayoutParams();
779 | int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
780 | getPaddingLeft() + getPaddingRight(), p.width);
781 | int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
782 | getPaddingTop() + getPaddingBottom(), p.height);
783 | view.measure(childWidthSpec, childHeightSpec);
784 | measuredDimension[0] = view.getMeasuredWidth();
785 | measuredDimension[1] = view.getMeasuredHeight();
786 | mRecycler.recycleView(view);
787 | }
788 | }
789 |
790 | private boolean processRowSizeSecondary(boolean measure) {
791 | if (mFixedRowSizeSecondary != 0) {
792 | return false;
793 | }
794 |
795 | if (mGrid == null) {
796 | if (mState.getItemCount() > 0) {
797 | measureScrapChild(mFocusPosition == NO_POSITION ? 0 : mFocusPosition,
798 | MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
799 | MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
800 | mMeasuredDimension);
801 | if (DEBUG) Log.v(TAG, "measured scrap child: " + mMeasuredDimension[0] +
802 | " " + mMeasuredDimension[1]);
803 | } else {
804 | mMeasuredDimension[0] = mMeasuredDimension[1] = 0;
805 | }
806 | }
807 |
808 | List[] rows = mGrid == null ? null :
809 | mGrid.getItemPositionsInRows(mFirstVisiblePos, mLastVisiblePos);
810 | boolean changed = false;
811 |
812 | for (int rowIndex = 0; rowIndex < mNumRows; rowIndex++) {
813 | int rowSize = 0;
814 |
815 | final int rowItemCount = rows == null ? 1 : rows[rowIndex].size();
816 | if (DEBUG) Log.v(getTag(), "processRowSizeSecondary row " + rowIndex +
817 | " rowItemCount " + rowItemCount);
818 |
819 | for (int i = 0; i < rowItemCount; i++) {
820 | if (rows != null) {
821 | final int position = rows[rowIndex].get(i);
822 | final View view = findViewByPosition(position);
823 | if (measure && view.isLayoutRequested()) {
824 | measureChild(view);
825 | }
826 | mMeasuredDimension[0] = view.getMeasuredWidth();
827 | mMeasuredDimension[1] = view.getMeasuredHeight();
828 | }
829 | final int secondarySize = mOrientation == HORIZONTAL ?
830 | mMeasuredDimension[1] : mMeasuredDimension[0];
831 | if (secondarySize > rowSize) {
832 | rowSize = secondarySize;
833 | }
834 | }
835 | if (DEBUG) Log.v(getTag(), "row " + rowIndex + " rowItemCount " + rowItemCount +
836 | " rowSize " + rowSize);
837 |
838 | if (mRowSizeSecondary[rowIndex] != rowSize) {
839 | if (DEBUG) Log.v(getTag(), "row size secondary changed: " + mRowSizeSecondary[rowIndex] +
840 | ", " + rowSize);
841 |
842 | mRowSizeSecondary[rowIndex] = rowSize;
843 | changed = true;
844 | }
845 | }
846 |
847 | return changed;
848 | }
849 |
850 | /**
851 | * Checks if we need to update row secondary sizes.
852 | */
853 | private void updateRowSecondarySizeRefresh() {
854 | mRowSecondarySizeRefresh = processRowSizeSecondary(false);
855 | if (mRowSecondarySizeRefresh) {
856 | if (DEBUG) Log.v(getTag(), "mRowSecondarySizeRefresh now set");
857 | forceRequestLayout();
858 | }
859 | }
860 |
861 | private void forceRequestLayout() {
862 | if (DEBUG) Log.v(getTag(), "forceRequestLayout");
863 | // RecyclerView prevents us from requesting layout in many cases
864 | // (during layout, during scroll, etc.)
865 | // For secondary row size wrap_content support we currently need a
866 | // second layout pass to update the measured size after having measured
867 | // and added child views in layoutChildren.
868 | // Force the second layout by posting a delayed runnable.
869 | // TODO: investigate allowing a second layout pass,
870 | // or move child add/measure logic to the measure phase.
871 | mBaseGridView.getHandler().post(new Runnable() {
872 | @Override
873 | public void run() {
874 | if (DEBUG) Log.v(getTag(), "request Layout from runnable");
875 | requestLayout();
876 | }
877 | });
878 | }
879 |
880 | @Override
881 | public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
882 | saveContext(recycler, state);
883 |
884 | int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary;
885 | int measuredSizeSecondary;
886 | if (mOrientation == HORIZONTAL) {
887 | sizePrimary = MeasureSpec.getSize(widthSpec);
888 | sizeSecondary = MeasureSpec.getSize(heightSpec);
889 | modeSecondary = MeasureSpec.getMode(heightSpec);
890 | paddingSecondary = getPaddingTop() + getPaddingBottom();
891 | } else {
892 | sizeSecondary = MeasureSpec.getSize(widthSpec);
893 | sizePrimary = MeasureSpec.getSize(heightSpec);
894 | modeSecondary = MeasureSpec.getMode(widthSpec);
895 | paddingSecondary = getPaddingLeft() + getPaddingRight();
896 | }
897 | if (DEBUG) Log.v(getTag(), "onMeasure widthSpec " + Integer.toHexString(widthSpec) +
898 | " heightSpec " + Integer.toHexString(heightSpec) +
899 | " modeSecondary " + Integer.toHexString(modeSecondary) +
900 | " sizeSecondary " + sizeSecondary + " " + this);
901 |
902 | mMaxSizeSecondary = sizeSecondary;
903 |
904 | if (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) {
905 | mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
906 | mFixedRowSizeSecondary = 0;
907 |
908 | if (mRowSizeSecondary == null || mRowSizeSecondary.length != mNumRows) {
909 | mRowSizeSecondary = new int[mNumRows];
910 | }
911 |
912 | // Measure all current children and update cached row heights
913 | processRowSizeSecondary(true);
914 |
915 | switch (modeSecondary) {
916 | case MeasureSpec.UNSPECIFIED:
917 | measuredSizeSecondary = getSizeSecondary() + paddingSecondary;
918 | break;
919 | case MeasureSpec.AT_MOST:
920 | measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary,
921 | mMaxSizeSecondary);
922 | break;
923 | case MeasureSpec.EXACTLY:
924 | measuredSizeSecondary = mMaxSizeSecondary;
925 | break;
926 | default:
927 | throw new IllegalStateException("wrong spec");
928 | }
929 |
930 | } else {
931 | switch (modeSecondary) {
932 | case MeasureSpec.UNSPECIFIED:
933 | if (mRowSizeSecondaryRequested == 0) {
934 | if (mOrientation == HORIZONTAL) {
935 | throw new IllegalStateException("Must specify rowHeight or view height");
936 | } else {
937 | throw new IllegalStateException("Must specify columnWidth or view width");
938 | }
939 | }
940 | mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
941 | mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
942 | measuredSizeSecondary = mFixedRowSizeSecondary * mNumRows + mMarginSecondary
943 | * (mNumRows - 1) + paddingSecondary;
944 | break;
945 | case MeasureSpec.AT_MOST:
946 | case MeasureSpec.EXACTLY:
947 | if (mNumRowsRequested == 0 && mRowSizeSecondaryRequested == 0) {
948 | mNumRows = 1;
949 | mFixedRowSizeSecondary = sizeSecondary - paddingSecondary;
950 | } else if (mNumRowsRequested == 0) {
951 | mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
952 | mNumRows = (sizeSecondary + mMarginSecondary)
953 | / (mRowSizeSecondaryRequested + mMarginSecondary);
954 | } else if (mRowSizeSecondaryRequested == 0) {
955 | mNumRows = mNumRowsRequested;
956 | mFixedRowSizeSecondary = (sizeSecondary - paddingSecondary - mMarginSecondary
957 | * (mNumRows - 1)) / mNumRows;
958 | } else {
959 | mNumRows = mNumRowsRequested;
960 | mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
961 | }
962 | measuredSizeSecondary = sizeSecondary;
963 | if (modeSecondary == MeasureSpec.AT_MOST) {
964 | int childrenSize = mFixedRowSizeSecondary * mNumRows + mMarginSecondary
965 | * (mNumRows - 1) + paddingSecondary;
966 | if (childrenSize < measuredSizeSecondary) {
967 | measuredSizeSecondary = childrenSize;
968 | }
969 | }
970 | break;
971 | default:
972 | throw new IllegalStateException("wrong spec");
973 | }
974 | }
975 | if (mOrientation == HORIZONTAL) {
976 | setMeasuredDimension(sizePrimary, measuredSizeSecondary);
977 | } else {
978 | setMeasuredDimension(measuredSizeSecondary, sizePrimary);
979 | }
980 | if (DEBUG) {
981 | Log.v(getTag(), "onMeasure sizePrimary " + sizePrimary +
982 | " measuredSizeSecondary " + measuredSizeSecondary +
983 | " mFixedRowSizeSecondary " + mFixedRowSizeSecondary +
984 | " mNumRows " + mNumRows);
985 | }
986 |
987 | leaveContext();
988 | }
989 |
990 | private void measureChild(View child) {
991 | final ViewGroup.LayoutParams lp = child.getLayoutParams();
992 | final int secondarySpec = (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) ?
993 | MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) :
994 | MeasureSpec.makeMeasureSpec(mFixedRowSizeSecondary, MeasureSpec.EXACTLY);
995 | int widthSpec, heightSpec;
996 |
997 | if (mOrientation == HORIZONTAL) {
998 | widthSpec = ViewGroup.getChildMeasureSpec(
999 | MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1000 | 0, lp.width);
1001 | heightSpec = ViewGroup.getChildMeasureSpec(secondarySpec, 0, lp.height);
1002 | } else {
1003 | heightSpec = ViewGroup.getChildMeasureSpec(
1004 | MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1005 | 0, lp.height);
1006 | widthSpec = ViewGroup.getChildMeasureSpec(secondarySpec, 0, lp.width);
1007 | }
1008 |
1009 | child.measure(widthSpec, heightSpec);
1010 |
1011 | if (DEBUG) Log.v(getTag(), "measureChild secondarySpec " + Integer.toHexString(secondarySpec) +
1012 | " widthSpec " + Integer.toHexString(widthSpec) +
1013 | " heightSpec " + Integer.toHexString(heightSpec) +
1014 | " measuredWidth " + child.getMeasuredWidth() +
1015 | " measuredHeight " + child.getMeasuredHeight());
1016 | if (DEBUG) Log.v(getTag(), "child lp width " + lp.width + " height " + lp.height);
1017 | }
1018 |
1019 | private StaggeredGrid.Provider mGridProvider = new StaggeredGrid.Provider() {
1020 |
1021 | @Override
1022 | public int getCount() {
1023 | return mState.getItemCount();
1024 | }
1025 |
1026 | @Override
1027 | public void createItem(int index, int rowIndex, boolean append) {
1028 | View v = getViewForPosition(index);
1029 | if (mFirstVisiblePos >= 0) {
1030 | // when StaggeredGrid append or prepend item, we must guarantee
1031 | // that sibling item has created views already.
1032 | if (append && index != mLastVisiblePos + 1) {
1033 | throw new RuntimeException();
1034 | } else if (!append && index != mFirstVisiblePos - 1) {
1035 | throw new RuntimeException();
1036 | }
1037 | }
1038 |
1039 | // See recyclerView docs: we don't need re-add scraped view if it was removed.
1040 | if (!((RecyclerView.LayoutParams) v.getLayoutParams()).isItemRemoved()) {
1041 | if (append) {
1042 | addView(v);
1043 | } else {
1044 | addView(v, 0);
1045 | }
1046 | measureChild(v);
1047 | }
1048 |
1049 | int length = mOrientation == HORIZONTAL ? v.getMeasuredWidth() : v.getMeasuredHeight();
1050 | int start, end;
1051 | if (append) {
1052 | start = mRows[rowIndex].high;
1053 | if (start != mRows[rowIndex].low) {
1054 | // if there are existing item in the row, add margin between
1055 | start += mMarginPrimary;
1056 | } else {
1057 | final int lastRow = mRows.length - 1;
1058 | if (lastRow != rowIndex && mRows[lastRow].high != mRows[lastRow].low) {
1059 | // if there are existing item in the last row, insert
1060 | // the new item after the last item of last row.
1061 | start = mRows[lastRow].high + mMarginPrimary;
1062 | }
1063 | }
1064 | end = start + length;
1065 | mRows[rowIndex].high = end;
1066 | } else {
1067 | end = mRows[rowIndex].low;
1068 | if (end != mRows[rowIndex].high) {
1069 | end -= mMarginPrimary;
1070 | } else if (0 != rowIndex && mRows[0].high != mRows[0].low) {
1071 | // if there are existing item in the first row, insert
1072 | // the new item before the first item of first row.
1073 | end = mRows[0].low - mMarginPrimary;
1074 | }
1075 | start = end - length;
1076 | mRows[rowIndex].low = start;
1077 | }
1078 | if (mFirstVisiblePos < 0) {
1079 | mFirstVisiblePos = mLastVisiblePos = index;
1080 | } else {
1081 | if (append) {
1082 | mLastVisiblePos++;
1083 | } else {
1084 | mFirstVisiblePos--;
1085 | }
1086 | }
1087 | if (DEBUG) Log.v(getTag(), "start " + start + " end " + end);
1088 | int startSecondary = getRowStartSecondary(rowIndex) - mScrollOffsetSecondary;
1089 | layoutChild(rowIndex, v, start - mScrollOffsetPrimary, end - mScrollOffsetPrimary,
1090 | startSecondary);
1091 | if (DEBUG) {
1092 | Log.d(getTag(), "addView " + index + " " + v);
1093 | }
1094 | if (index == mFirstVisiblePos) {
1095 | updateScrollMin();
1096 | }
1097 | if (index == mLastVisiblePos) {
1098 | updateScrollMax();
1099 | }
1100 | }
1101 | };
1102 |
1103 | private void layoutChild(int rowIndex, View v, int start, int end, int startSecondary) {
1104 | int sizeSecondary = mOrientation == HORIZONTAL ? v.getMeasuredHeight()
1105 | : v.getMeasuredWidth();
1106 | if (mFixedRowSizeSecondary > 0) {
1107 | sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary);
1108 | }
1109 | final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1110 | final int horizontalGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1111 | if (mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP
1112 | || mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT) {
1113 | // do nothing
1114 | } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM
1115 | || mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT) {
1116 | startSecondary += getRowSizeSecondary(rowIndex) - sizeSecondary;
1117 | } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL
1118 | || mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL) {
1119 | startSecondary += (getRowSizeSecondary(rowIndex) - sizeSecondary) / 2;
1120 | }
1121 | int left, top, right, bottom;
1122 | if (mOrientation == HORIZONTAL) {
1123 | left = start;
1124 | top = startSecondary;
1125 | right = end;
1126 | bottom = startSecondary + sizeSecondary;
1127 | } else {
1128 | top = start;
1129 | left = startSecondary;
1130 | bottom = end;
1131 | right = startSecondary + sizeSecondary;
1132 | }
1133 | v.layout(left, top, right, bottom);
1134 | updateChildOpticalInsets(v, left, top, right, bottom);
1135 | updateChildAlignments(v);
1136 | }
1137 |
1138 | private void updateChildOpticalInsets(View v, int left, int top, int right, int bottom) {
1139 | LayoutParams p = (LayoutParams) v.getLayoutParams();
1140 | p.setOpticalInsets(left - v.getLeft(), top - v.getTop(),
1141 | v.getRight() - right, v.getBottom() - bottom);
1142 | }
1143 |
1144 | private void updateChildAlignments(View v) {
1145 | LayoutParams p = (LayoutParams) v.getLayoutParams();
1146 | p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
1147 | p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
1148 | }
1149 |
1150 | private void updateChildAlignments() {
1151 | for (int i = 0, c = getChildCount(); i < c; i++) {
1152 | updateChildAlignments(getChildAt(i));
1153 | }
1154 | }
1155 |
1156 | private boolean needsAppendVisibleItem() {
1157 | if (mLastVisiblePos < mFocusPosition) {
1158 | return true;
1159 | }
1160 | int right = mScrollOffsetPrimary + mSizePrimary;
1161 | for (int i = 0; i < mNumRows; i++) {
1162 | if (mRows[i].low == mRows[i].high) {
1163 | if (mRows[i].high < right) {
1164 | return true;
1165 | }
1166 | } else if (mRows[i].high < right - mMarginPrimary) {
1167 | return true;
1168 | }
1169 | }
1170 | return false;
1171 | }
1172 |
1173 | private boolean needsPrependVisibleItem() {
1174 | if (mFirstVisiblePos > mFocusPosition) {
1175 | return true;
1176 | }
1177 | for (int i = 0; i < mNumRows; i++) {
1178 | if (mRows[i].low == mRows[i].high) {
1179 | if (mRows[i].low > mScrollOffsetPrimary) {
1180 | return true;
1181 | }
1182 | } else if (mRows[i].low - mMarginPrimary > mScrollOffsetPrimary) {
1183 | return true;
1184 | }
1185 | }
1186 | return false;
1187 | }
1188 |
1189 | // Append one column if possible and return true if reach end.
1190 | private boolean appendOneVisibleItem() {
1191 | while (true) {
1192 | if (mLastVisiblePos != NO_POSITION && mLastVisiblePos < mState.getItemCount() -1 &&
1193 | mLastVisiblePos < mGrid.getLastIndex()) {
1194 | // append invisible view of saved location till last row
1195 | final int index = mLastVisiblePos + 1;
1196 | final int row = mGrid.getLocation(index).row;
1197 | mGridProvider.createItem(index, row, true);
1198 | if (row == mNumRows - 1) {
1199 | return false;
1200 | }
1201 | } else if ((mLastVisiblePos == NO_POSITION && mState.getItemCount() > 0) ||
1202 | (mLastVisiblePos != NO_POSITION &&
1203 | mLastVisiblePos < mState.getItemCount() - 1)) {
1204 | mGrid.appendItems(mScrollOffsetPrimary + mSizePrimary);
1205 | return false;
1206 | } else {
1207 | return true;
1208 | }
1209 | }
1210 | }
1211 |
1212 | private void appendVisibleItems() {
1213 | while (needsAppendVisibleItem()) {
1214 | if (appendOneVisibleItem()) {
1215 | break;
1216 | }
1217 | }
1218 | }
1219 |
1220 | // Prepend one column if possible and return true if reach end.
1221 | private boolean prependOneVisibleItem() {
1222 | while (true) {
1223 | if (mFirstVisiblePos > 0) {
1224 | if (mFirstVisiblePos > mGrid.getFirstIndex()) {
1225 | // prepend invisible view of saved location till first row
1226 | final int index = mFirstVisiblePos - 1;
1227 | final int row = mGrid.getLocation(index).row;
1228 | mGridProvider.createItem(index, row, false);
1229 | if (row == 0) {
1230 | return false;
1231 | }
1232 | } else {
1233 | mGrid.prependItems(mScrollOffsetPrimary);
1234 | return false;
1235 | }
1236 | } else {
1237 | return true;
1238 | }
1239 | }
1240 | }
1241 |
1242 | private void prependVisibleItems() {
1243 | while (needsPrependVisibleItem()) {
1244 | if (prependOneVisibleItem()) {
1245 | break;
1246 | }
1247 | }
1248 | }
1249 |
1250 | private void removeChildAt(int position) {
1251 | View v = findViewByPosition(position);
1252 | if (v != null) {
1253 | if (DEBUG) {
1254 | Log.d(getTag(), "removeAndRecycleViewAt " + position);
1255 | }
1256 | removeAndRecycleView(v, mRecycler);
1257 | }
1258 | }
1259 |
1260 | private void removeInvisibleViewsAtEnd() {
1261 | if (!mPruneChild) {
1262 | return;
1263 | }
1264 | boolean update = false;
1265 | while(mLastVisiblePos > mFirstVisiblePos && mLastVisiblePos > mFocusPosition) {
1266 | View view = findViewByPosition(mLastVisiblePos);
1267 | if (getViewMin(view) > mSizePrimary) {
1268 | removeChildAt(mLastVisiblePos);
1269 | mLastVisiblePos--;
1270 | update = true;
1271 | } else {
1272 | break;
1273 | }
1274 | }
1275 | if (update) {
1276 | updateRowsMinMax();
1277 | }
1278 | }
1279 |
1280 | private void removeInvisibleViewsAtFront() {
1281 | if (!mPruneChild) {
1282 | return;
1283 | }
1284 | boolean update = false;
1285 | while(mLastVisiblePos > mFirstVisiblePos && mFirstVisiblePos < mFocusPosition) {
1286 | View view = findViewByPosition(mFirstVisiblePos);
1287 | if (getViewMax(view) < 0) {
1288 | removeChildAt(mFirstVisiblePos);
1289 | mFirstVisiblePos++;
1290 | update = true;
1291 | } else {
1292 | break;
1293 | }
1294 | }
1295 | if (update) {
1296 | updateRowsMinMax();
1297 | }
1298 | }
1299 |
1300 | private void updateRowsMinMax() {
1301 | if (mFirstVisiblePos < 0) {
1302 | return;
1303 | }
1304 | for (int i = 0; i < mNumRows; i++) {
1305 | mRows[i].low = Integer.MAX_VALUE;
1306 | mRows[i].high = Integer.MIN_VALUE;
1307 | }
1308 | for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) {
1309 | View view = findViewByPosition(i);
1310 | int row = mGrid.getLocation(i).row;
1311 | int low = getViewMin(view) + mScrollOffsetPrimary;
1312 | if (low < mRows[row].low) {
1313 | mRows[row].low = low;
1314 | }
1315 | int high = getViewMax(view) + mScrollOffsetPrimary;
1316 | if (high > mRows[row].high) {
1317 | mRows[row].high = high;
1318 | }
1319 | }
1320 | }
1321 |
1322 | // Fast layout when there is no structure change, adapter change, etc.
1323 | protected void fastRelayout() {
1324 | initScrollController();
1325 |
1326 | List[] rows = mGrid.getItemPositionsInRows(mFirstVisiblePos, mLastVisiblePos);
1327 |
1328 | // relayout and repositioning views on each row
1329 | for (int i = 0; i < mNumRows; i++) {
1330 | List row = rows[i];
1331 | final int startSecondary = getRowStartSecondary(i) - mScrollOffsetSecondary;
1332 | for (int j = 0, size = row.size(); j < size; j++) {
1333 | final int position = row.get(j);
1334 | final View view = findViewByPosition(position);
1335 | int primaryDelta, start, end;
1336 |
1337 | if (mOrientation == HORIZONTAL) {
1338 | final int primarySize = view.getMeasuredWidth();
1339 | if (view.isLayoutRequested()) {
1340 | measureChild(view);
1341 | }
1342 | start = getViewMin(view);
1343 | end = start + view.getMeasuredWidth();
1344 | primaryDelta = view.getMeasuredWidth() - primarySize;
1345 | if (primaryDelta != 0) {
1346 | for (int k = j + 1; k < size; k++) {
1347 | findViewByPosition(row.get(k)).offsetLeftAndRight(primaryDelta);
1348 | }
1349 | }
1350 | } else {
1351 | final int primarySize = view.getMeasuredHeight();
1352 | if (view.isLayoutRequested()) {
1353 | measureChild(view);
1354 | }
1355 | start = getViewMin(view);
1356 | end = start + view.getMeasuredHeight();
1357 | primaryDelta = view.getMeasuredHeight() - primarySize;
1358 | if (primaryDelta != 0) {
1359 | for (int k = j + 1; k < size; k++) {
1360 | findViewByPosition(row.get(k)).offsetTopAndBottom(primaryDelta);
1361 | }
1362 | }
1363 | }
1364 | layoutChild(i, view, start, end, startSecondary);
1365 | }
1366 | }
1367 |
1368 | updateRowsMinMax();
1369 | appendVisibleItems();
1370 | prependVisibleItems();
1371 |
1372 | updateRowsMinMax();
1373 | updateScrollMin();
1374 | updateScrollMax();
1375 | updateScrollSecondAxis();
1376 |
1377 | if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
1378 | View focusView = findViewByPosition(mFocusPosition == NO_POSITION ? 0 : mFocusPosition);
1379 | scrollToView(focusView, false);
1380 | }
1381 | }
1382 |
1383 | public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) {
1384 | if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount());
1385 | for (int i = getChildCount() - 1; i >= 0; i--) {
1386 | removeAndRecycleViewAt(i, recycler);
1387 | }
1388 | }
1389 |
1390 | // Lays out items based on the current scroll position
1391 | @Override
1392 | public void onLayoutChildren(Recycler recycler, State state) {
1393 | if (DEBUG) {
1394 | Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary "
1395 | + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary
1396 | + " inPreLayout " + state.isPreLayout()
1397 | + " didStructureChange " + state.didStructureChange()
1398 | + " mForceFullLayout " + mForceFullLayout);
1399 | Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
1400 | }
1401 |
1402 | if (mNumRows == 0) {
1403 | // haven't done measure yet
1404 | return;
1405 | }
1406 | final int itemCount = state.getItemCount();
1407 | if (itemCount < 0) {
1408 | return;
1409 | }
1410 |
1411 | if (!mLayoutEnabled) {
1412 | discardLayoutInfo();
1413 | removeAndRecycleAllViews(recycler);
1414 | return;
1415 | }
1416 | mInLayout = true;
1417 |
1418 | saveContext(recycler, state);
1419 | // Track the old focus view so we can adjust our system scroll position
1420 | // so that any scroll animations happening now will remain valid.
1421 | // We must use same delta in Pre Layout (if prelayout exists) and second layout.
1422 | // So we cache the deltas in PreLayout and use it in second layout.
1423 | int delta = 0, deltaSecondary = 0;
1424 | if (!state.isPreLayout() && mUseDeltaInPreLayout) {
1425 | delta = mDeltaInPreLayout;
1426 | deltaSecondary = mDeltaSecondaryInPreLayout;
1427 | } else {
1428 | if (mFocusPosition != NO_POSITION
1429 | && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
1430 | View focusView = findViewByPosition(mFocusPosition);
1431 | if (focusView != null) {
1432 | delta = mWindowAlignment.mainAxis().getSystemScrollPos(
1433 | getViewCenter(focusView) + mScrollOffsetPrimary) - mScrollOffsetPrimary;
1434 | deltaSecondary =
1435 | mWindowAlignment.secondAxis().getSystemScrollPos(
1436 | getViewCenterSecondary(focusView) + mScrollOffsetSecondary)
1437 | - mScrollOffsetSecondary;
1438 | if (mUseDeltaInPreLayout = state.isPreLayout()) {
1439 | mDeltaInPreLayout = delta;
1440 | mDeltaSecondaryInPreLayout = deltaSecondary;
1441 | }
1442 | }
1443 | }
1444 | }
1445 |
1446 | final boolean hasDoneFirstLayout = hasDoneFirstLayout();
1447 | int savedFocusPos = mFocusPosition;
1448 | boolean fastRelayout = false;
1449 | if (!mState.didStructureChange() && !mForceFullLayout && hasDoneFirstLayout) {
1450 | fastRelayout = true;
1451 | fastRelayout();
1452 | } else {
1453 | boolean hadFocus = mBaseGridView.hasFocus();
1454 |
1455 | int newFocusPosition = init(mFocusPosition);
1456 | if (DEBUG) {
1457 | Log.v(getTag(), "mFocusPosition " + mFocusPosition + " newFocusPosition "
1458 | + newFocusPosition);
1459 | }
1460 |
1461 | // depending on result of init(), either recreating everything
1462 | // or try to reuse the row start positions near mFocusPosition
1463 | if (mGrid.getSize() == 0) {
1464 | // this is a fresh creating all items, starting from
1465 | // mFocusPosition with a estimated row index.
1466 | mGrid.setStart(newFocusPosition, StaggeredGrid.START_DEFAULT);
1467 |
1468 | // Can't track the old focus view
1469 | delta = deltaSecondary = 0;
1470 |
1471 | } else {
1472 | // mGrid remembers Locations for the column that
1473 | // contains mFocusePosition and also mRows remembers start
1474 | // positions of each row.
1475 | // Manually re-create child views for that column
1476 | int firstIndex = mGrid.getFirstIndex();
1477 | int lastIndex = mGrid.getLastIndex();
1478 | for (int i = firstIndex; i <= lastIndex; i++) {
1479 | mGridProvider.createItem(i, mGrid.getLocation(i).row, true);
1480 | }
1481 | }
1482 | // add visible views at end until reach the end of window
1483 | appendVisibleItems();
1484 | // add visible views at front until reach the start of window
1485 | prependVisibleItems();
1486 | // multiple rounds: scrollToView of first round may drag first/last child into
1487 | // "visible window" and we update scrollMin/scrollMax then run second scrollToView
1488 | int oldFirstVisible;
1489 | int oldLastVisible;
1490 | do {
1491 | oldFirstVisible = mFirstVisiblePos;
1492 | oldLastVisible = mLastVisiblePos;
1493 | View focusView = findViewByPosition(newFocusPosition);
1494 | // we need force to initialize the child view's position
1495 | scrollToView(focusView, false);
1496 | if (focusView != null && hadFocus) {
1497 | focusView.requestFocus();
1498 | }
1499 | appendVisibleItems();
1500 | prependVisibleItems();
1501 | removeInvisibleViewsAtFront();
1502 | removeInvisibleViewsAtEnd();
1503 | } while (mFirstVisiblePos != oldFirstVisible || mLastVisiblePos != oldLastVisible);
1504 | }
1505 | mForceFullLayout = false;
1506 |
1507 | if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
1508 | scrollDirectionPrimary(-delta);
1509 | scrollDirectionSecondary(-deltaSecondary);
1510 | }
1511 | appendVisibleItems();
1512 | prependVisibleItems();
1513 | removeInvisibleViewsAtFront();
1514 | removeInvisibleViewsAtEnd();
1515 |
1516 | if (DEBUG) {
1517 | StringWriter sw = new StringWriter();
1518 | PrintWriter pw = new PrintWriter(sw);
1519 | mGrid.debugPrint(pw);
1520 | Log.d(getTag(), sw.toString());
1521 | }
1522 |
1523 | if (mRowSecondarySizeRefresh) {
1524 | mRowSecondarySizeRefresh = false;
1525 | } else {
1526 | updateRowSecondarySizeRefresh();
1527 | }
1528 |
1529 | if (!state.isPreLayout()) {
1530 | mUseDeltaInPreLayout = false;
1531 | if (!fastRelayout || mFocusPosition != savedFocusPos) {
1532 | dispatchChildSelected();
1533 | }
1534 | }
1535 | mInLayout = false;
1536 | leaveContext();
1537 | if (DEBUG) Log.v(getTag(), "layoutChildren end");
1538 | }
1539 |
1540 | private void offsetChildrenSecondary(int increment) {
1541 | final int childCount = getChildCount();
1542 | if (mOrientation == HORIZONTAL) {
1543 | for (int i = 0; i < childCount; i++) {
1544 | getChildAt(i).offsetTopAndBottom(increment);
1545 | }
1546 | } else {
1547 | for (int i = 0; i < childCount; i++) {
1548 | getChildAt(i).offsetLeftAndRight(increment);
1549 | }
1550 | }
1551 | }
1552 |
1553 | private void offsetChildrenPrimary(int increment) {
1554 | final int childCount = getChildCount();
1555 | if (mOrientation == VERTICAL) {
1556 | for (int i = 0; i < childCount; i++) {
1557 | getChildAt(i).offsetTopAndBottom(increment);
1558 | }
1559 | } else {
1560 | for (int i = 0; i < childCount; i++) {
1561 | getChildAt(i).offsetLeftAndRight(increment);
1562 | }
1563 | }
1564 | }
1565 |
1566 | @Override
1567 | public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
1568 | if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx);
1569 | if (!mLayoutEnabled || !hasDoneFirstLayout()) {
1570 | return 0;
1571 | }
1572 | saveContext(recycler, state);
1573 | int result;
1574 | if (mOrientation == HORIZONTAL) {
1575 | result = scrollDirectionPrimary(dx);
1576 | } else {
1577 | result = scrollDirectionSecondary(dx);
1578 | }
1579 | leaveContext();
1580 | return result;
1581 | }
1582 |
1583 | @Override
1584 | public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
1585 | if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy);
1586 | if (!mLayoutEnabled || !hasDoneFirstLayout()) {
1587 | return 0;
1588 | }
1589 | saveContext(recycler, state);
1590 | int result;
1591 | if (mOrientation == VERTICAL) {
1592 | result = scrollDirectionPrimary(dy);
1593 | } else {
1594 | result = scrollDirectionSecondary(dy);
1595 | }
1596 | leaveContext();
1597 | return result;
1598 | }
1599 |
1600 | // scroll in main direction may add/prune views
1601 | private int scrollDirectionPrimary(int da) {
1602 | if (da > 0) {
1603 | if (!mWindowAlignment.mainAxis().isMaxUnknown()) {
1604 | int maxScroll = mWindowAlignment.mainAxis().getMaxScroll();
1605 | if (mScrollOffsetPrimary + da > maxScroll) {
1606 | da = maxScroll - mScrollOffsetPrimary;
1607 | }
1608 | }
1609 | } else if (da < 0) {
1610 | if (!mWindowAlignment.mainAxis().isMinUnknown()) {
1611 | int minScroll = mWindowAlignment.mainAxis().getMinScroll();
1612 | if (mScrollOffsetPrimary + da < minScroll) {
1613 | da = minScroll - mScrollOffsetPrimary;
1614 | }
1615 | }
1616 | }
1617 | if (da == 0) {
1618 | return 0;
1619 | }
1620 | offsetChildrenPrimary(-da);
1621 | mScrollOffsetPrimary += da;
1622 | if (mInLayout) {
1623 | return da;
1624 | }
1625 |
1626 | int childCount = getChildCount();
1627 | boolean updated;
1628 |
1629 | if (da > 0) {
1630 | appendVisibleItems();
1631 | } else if (da < 0) {
1632 | prependVisibleItems();
1633 | }
1634 | updated = getChildCount() > childCount;
1635 | childCount = getChildCount();
1636 |
1637 | if (da > 0) {
1638 | removeInvisibleViewsAtFront();
1639 | } else if (da < 0) {
1640 | removeInvisibleViewsAtEnd();
1641 | }
1642 | updated |= getChildCount() < childCount;
1643 |
1644 | if (updated) {
1645 | updateRowSecondarySizeRefresh();
1646 | }
1647 |
1648 | mBaseGridView.invalidate();
1649 | return da;
1650 | }
1651 |
1652 | // scroll in second direction will not add/prune views
1653 | private int scrollDirectionSecondary(int dy) {
1654 | if (dy == 0) {
1655 | return 0;
1656 | }
1657 | offsetChildrenSecondary(-dy);
1658 | mScrollOffsetSecondary += dy;
1659 | mBaseGridView.invalidate();
1660 | return dy;
1661 | }
1662 |
1663 | private void updateScrollMax() {
1664 | if (mLastVisiblePos < 0) {
1665 | return;
1666 | }
1667 | final boolean lastAvailable = mLastVisiblePos == mState.getItemCount() - 1;
1668 | final boolean maxUnknown = mWindowAlignment.mainAxis().isMaxUnknown();
1669 | if (!lastAvailable && maxUnknown) {
1670 | return;
1671 | }
1672 | int maxEdge = Integer.MIN_VALUE;
1673 | int rowIndex = -1;
1674 | for (int i = 0; i < mRows.length; i++) {
1675 | if (mRows[i].high > maxEdge) {
1676 | maxEdge = mRows[i].high;
1677 | rowIndex = i;
1678 | }
1679 | }
1680 | int maxScroll = Integer.MAX_VALUE;
1681 | for (int i = mLastVisiblePos; i >= mFirstVisiblePos; i--) {
1682 | StaggeredGrid.Location location = mGrid.getLocation(i);
1683 | if (location != null && location.row == rowIndex) {
1684 | int savedMaxEdge = mWindowAlignment.mainAxis().getMaxEdge();
1685 | mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1686 | maxScroll = mWindowAlignment
1687 | .mainAxis().getSystemScrollPos(mScrollOffsetPrimary
1688 | + getViewCenter(findViewByPosition(i)));
1689 | mWindowAlignment.mainAxis().setMaxEdge(savedMaxEdge);
1690 | break;
1691 | }
1692 | }
1693 | if (lastAvailable) {
1694 | mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1695 | mWindowAlignment.mainAxis().setMaxScroll(maxScroll);
1696 | if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge +
1697 | " scrollMax to " + maxScroll);
1698 | } else {
1699 | // the maxScroll for currently last visible item is larger,
1700 | // so we must invalidate the max scroll value.
1701 | if (maxScroll > mWindowAlignment.mainAxis().getMaxScroll()) {
1702 | mWindowAlignment.mainAxis().invalidateScrollMax();
1703 | if (DEBUG) Log.v(getTag(), "Invalidate scrollMax since it should be "
1704 | + "greater than " + maxScroll);
1705 | }
1706 | }
1707 | }
1708 |
1709 | private void updateScrollMin() {
1710 | if (mFirstVisiblePos < 0) {
1711 | return;
1712 | }
1713 | final boolean firstAvailable = mFirstVisiblePos == 0;
1714 | final boolean minUnknown = mWindowAlignment.mainAxis().isMinUnknown();
1715 | if (!firstAvailable && minUnknown) {
1716 | return;
1717 | }
1718 | int minEdge = Integer.MAX_VALUE;
1719 | int rowIndex = -1;
1720 | for (int i = 0; i < mRows.length; i++) {
1721 | if (mRows[i].low < minEdge) {
1722 | minEdge = mRows[i].low;
1723 | rowIndex = i;
1724 | }
1725 | }
1726 | int minScroll = Integer.MIN_VALUE;
1727 | for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) {
1728 | StaggeredGrid.Location location = mGrid.getLocation(i);
1729 | if (location != null && location.row == rowIndex) {
1730 | int savedMinEdge = mWindowAlignment.mainAxis().getMinEdge();
1731 | mWindowAlignment.mainAxis().setMinEdge(minEdge);
1732 | minScroll = mWindowAlignment
1733 | .mainAxis().getSystemScrollPos(mScrollOffsetPrimary
1734 | + getViewCenter(findViewByPosition(i)));
1735 | mWindowAlignment.mainAxis().setMinEdge(savedMinEdge);
1736 | break;
1737 | }
1738 | }
1739 | if (firstAvailable) {
1740 | mWindowAlignment.mainAxis().setMinEdge(minEdge);
1741 | mWindowAlignment.mainAxis().setMinScroll(minScroll);
1742 | if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge +
1743 | " scrollMin to " + minScroll);
1744 | } else {
1745 | // the minScroll for currently first visible item is smaller,
1746 | // so we must invalidate the min scroll value.
1747 | if (minScroll < mWindowAlignment.mainAxis().getMinScroll()) {
1748 | mWindowAlignment.mainAxis().invalidateScrollMin();
1749 | if (DEBUG) Log.v(getTag(), "Invalidate scrollMin, since it should be "
1750 | + "less than " + minScroll);
1751 | }
1752 | }
1753 | }
1754 |
1755 | private void updateScrollSecondAxis() {
1756 | mWindowAlignment.secondAxis().setMinEdge(0);
1757 | mWindowAlignment.secondAxis().setMaxEdge(getSizeSecondary());
1758 | }
1759 |
1760 | private void initScrollController() {
1761 | mWindowAlignment.horizontal.setSize(getWidth());
1762 | mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1763 | mWindowAlignment.vertical.setSize(getHeight());
1764 | mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1765 | mSizePrimary = mWindowAlignment.mainAxis().getSize();
1766 |
1767 | if (DEBUG) {
1768 | Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary
1769 | + " mWindowAlignment " + mWindowAlignment);
1770 | }
1771 | }
1772 |
1773 | public void setSelection(RecyclerView parent, int position) {
1774 | setSelection(parent, position, false);
1775 | }
1776 |
1777 | public void setSelectionSmooth(RecyclerView parent, int position) {
1778 | setSelection(parent, position, true);
1779 | }
1780 |
1781 | public int getSelection() {
1782 | return mFocusPosition;
1783 | }
1784 |
1785 | public void setSelection(RecyclerView parent, int position, boolean smooth) {
1786 | if (mFocusPosition == position) {
1787 | return;
1788 | }
1789 | View view = findViewByPosition(position);
1790 | if (view != null) {
1791 | scrollToView(view, smooth);
1792 | } else {
1793 | mFocusPosition = position;
1794 | if (!mLayoutEnabled) {
1795 | return;
1796 | }
1797 | if (smooth) {
1798 | if (!hasDoneFirstLayout()) {
1799 | Log.w(getTag(), "setSelectionSmooth should " +
1800 | "not be called before first layout pass");
1801 | return;
1802 | }
1803 | LinearSmoothScroller linearSmoothScroller =
1804 | new LinearSmoothScroller(parent.getContext()) {
1805 | @Override
1806 | public PointF computeScrollVectorForPosition(int targetPosition) {
1807 | if (getChildCount() == 0) {
1808 | return null;
1809 | }
1810 | final int firstChildPos = getPosition(getChildAt(0));
1811 | final int direction = targetPosition < firstChildPos ? -1 : 1;
1812 | if (mOrientation == HORIZONTAL) {
1813 | return new PointF(direction, 0);
1814 | } else {
1815 | return new PointF(0, direction);
1816 | }
1817 | }
1818 | @Override
1819 | protected void onTargetFound(View targetView,
1820 | State state, Action action) {
1821 | if (hasFocus()) {
1822 | targetView.requestFocus();
1823 | } else {
1824 | dispatchChildSelected();
1825 | }
1826 | if (getScrollPosition(targetView, mTempDeltas)) {
1827 | int dx, dy;
1828 | if (mOrientation == HORIZONTAL) {
1829 | dx = mTempDeltas[0];
1830 | dy = mTempDeltas[1];
1831 | } else {
1832 | dx = mTempDeltas[1];
1833 | dy = mTempDeltas[0];
1834 | }
1835 | final int distance = (int) Math.sqrt(dx * dx + dy * dy);
1836 | final int time = calculateTimeForDeceleration(distance);
1837 | action.update(dx, dy, time, mDecelerateInterpolator);
1838 | }
1839 | }
1840 | };
1841 | linearSmoothScroller.setTargetPosition(position);
1842 | startSmoothScroll(linearSmoothScroller);
1843 | } else {
1844 | mForceFullLayout = true;
1845 | parent.requestLayout();
1846 | }
1847 | }
1848 | }
1849 |
1850 | @Override
1851 | public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
1852 | boolean needsLayout = false;
1853 | if (itemCount != 0) {
1854 | if (mFirstVisiblePos < 0) {
1855 | needsLayout = true;
1856 | } else if (!(positionStart > mLastVisiblePos + 1 ||
1857 | positionStart + itemCount < mFirstVisiblePos - 1)) {
1858 | needsLayout = true;
1859 | }
1860 | }
1861 | if (needsLayout) {
1862 | recyclerView.requestLayout();
1863 | }
1864 | }
1865 |
1866 | @Override
1867 | public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
1868 | if (mFocusSearchDisabled) {
1869 | return true;
1870 | }
1871 | if (!mInLayout) {
1872 | scrollToView(child, true);
1873 | }
1874 | return true;
1875 | }
1876 |
1877 | @Override
1878 | public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
1879 | boolean immediate) {
1880 | if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
1881 | return false;
1882 | }
1883 |
1884 | int getScrollOffsetX() {
1885 | return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
1886 | }
1887 |
1888 | int getScrollOffsetY() {
1889 | return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
1890 | }
1891 |
1892 | public void getViewSelectedOffsets(View view, int[] offsets) {
1893 | int scrollOffsetX = getScrollOffsetX();
1894 | int scrollOffsetY = getScrollOffsetY();
1895 | int viewCenterX = scrollOffsetX + getViewCenterX(view);
1896 | int viewCenterY = scrollOffsetY + getViewCenterY(view);
1897 | offsets[0] = mWindowAlignment.horizontal.getSystemScrollPos(viewCenterX) - scrollOffsetX;
1898 | offsets[1] = mWindowAlignment.vertical.getSystemScrollPos(viewCenterY) - scrollOffsetY;
1899 | }
1900 |
1901 | /**
1902 | * Scroll to a given child view and change mFocusPosition.
1903 | */
1904 | private void scrollToView(View view, boolean smooth) {
1905 | int newFocusPosition = getPositionByView(view);
1906 | if (newFocusPosition != mFocusPosition) {
1907 | mFocusPosition = newFocusPosition;
1908 | if (!mInLayout) {
1909 | dispatchChildSelected();
1910 | }
1911 | }
1912 | if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
1913 | mBaseGridView.invalidate();
1914 | }
1915 | if (view == null) {
1916 | return;
1917 | }
1918 | if (!view.hasFocus() && mBaseGridView.hasFocus()) {
1919 | // transfer focus to the child if it does not have focus yet (e.g. triggered
1920 | // by setSelection())
1921 | view.requestFocus();
1922 | }
1923 | if (getScrollPosition(view, mTempDeltas)) {
1924 | scrollGrid(mTempDeltas[0], mTempDeltas[1], smooth);
1925 | }
1926 | }
1927 |
1928 | private boolean getScrollPosition(View view, int[] deltas) {
1929 | switch (mFocusScrollStrategy) {
1930 | case BaseGridView.FOCUS_SCROLL_ALIGNED:
1931 | default:
1932 | return getAlignedPosition(view, deltas);
1933 | case BaseGridView.FOCUS_SCROLL_ITEM:
1934 | case BaseGridView.FOCUS_SCROLL_PAGE:
1935 | return getNoneAlignedPosition(view, deltas);
1936 | }
1937 | }
1938 |
1939 | private boolean getNoneAlignedPosition(View view, int[] deltas) {
1940 | int pos = getPositionByView(view);
1941 | int viewMin = getViewMin(view);
1942 | int viewMax = getViewMax(view);
1943 | // we either align "firstView" to left/top padding edge
1944 | // or align "lastView" to right/bottom padding edge
1945 | View firstView = null;
1946 | View lastView = null;
1947 | int paddingLow = mWindowAlignment.mainAxis().getPaddingLow();
1948 | int clientSize = mWindowAlignment.mainAxis().getClientSize();
1949 | final int row = mGrid.getLocation(pos).row;
1950 | if (viewMin < paddingLow) {
1951 | // view enters low padding area:
1952 | firstView = view;
1953 | if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
1954 | // scroll one "page" left/top,
1955 | // align first visible item of the "page" at the low padding edge.
1956 | while (!prependOneVisibleItem()) {
1957 | List positions =
1958 | mGrid.getItemPositionsInRows(mFirstVisiblePos, pos)[row];
1959 | firstView = findViewByPosition(positions.get(0));
1960 | if (viewMax - getViewMin(firstView) > clientSize) {
1961 | if (positions.size() > 1) {
1962 | firstView = findViewByPosition(positions.get(1));
1963 | }
1964 | break;
1965 | }
1966 | }
1967 | }
1968 | } else if (viewMax > clientSize + paddingLow) {
1969 | // view enters high padding area:
1970 | if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
1971 | // scroll whole one page right/bottom, align view at the low padding edge.
1972 | firstView = view;
1973 | do {
1974 | List positions =
1975 | mGrid.getItemPositionsInRows(pos, mLastVisiblePos)[row];
1976 | lastView = findViewByPosition(positions.get(positions.size() - 1));
1977 | if (getViewMax(lastView) - viewMin > clientSize) {
1978 | lastView = null;
1979 | break;
1980 | }
1981 | } while (!appendOneVisibleItem());
1982 | if (lastView != null) {
1983 | // however if we reached end, we should align last view.
1984 | firstView = null;
1985 | }
1986 | } else {
1987 | lastView = view;
1988 | }
1989 | }
1990 | int scrollPrimary = 0;
1991 | int scrollSecondary = 0;
1992 | if (firstView != null) {
1993 | scrollPrimary = getViewMin(firstView) - paddingLow;
1994 | } else if (lastView != null) {
1995 | scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize);
1996 | }
1997 | View secondaryAlignedView;
1998 | if (firstView != null) {
1999 | secondaryAlignedView = firstView;
2000 | } else if (lastView != null) {
2001 | secondaryAlignedView = lastView;
2002 | } else {
2003 | secondaryAlignedView = view;
2004 | }
2005 | int viewCenterSecondary = mScrollOffsetSecondary +
2006 | getViewCenterSecondary(secondaryAlignedView);
2007 | scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos(viewCenterSecondary);
2008 | scrollSecondary -= mScrollOffsetSecondary;
2009 | if (scrollPrimary != 0 || scrollSecondary != 0) {
2010 | deltas[0] = scrollPrimary;
2011 | deltas[1] = scrollSecondary;
2012 | return true;
2013 | }
2014 | return false;
2015 | }
2016 |
2017 | private boolean getAlignedPosition(View view, int[] deltas) {
2018 | int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view);
2019 | int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view);
2020 |
2021 | int scrollPrimary = mWindowAlignment.mainAxis().getSystemScrollPos(viewCenterPrimary);
2022 | int scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos(viewCenterSecondary);
2023 | if (DEBUG) {
2024 | Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary
2025 | + " " + mWindowAlignment);
2026 | }
2027 | scrollPrimary -= mScrollOffsetPrimary;
2028 | scrollSecondary -= mScrollOffsetSecondary;
2029 | if (scrollPrimary != 0 || scrollSecondary != 0) {
2030 | deltas[0] = scrollPrimary;
2031 | deltas[1] = scrollSecondary;
2032 | return true;
2033 | }
2034 | return false;
2035 | }
2036 |
2037 | private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
2038 | if (mInLayout) {
2039 | scrollDirectionPrimary(scrollPrimary);
2040 | scrollDirectionSecondary(scrollSecondary);
2041 | } else {
2042 | int scrollX;
2043 | int scrollY;
2044 | if (mOrientation == HORIZONTAL) {
2045 | scrollX = scrollPrimary;
2046 | scrollY = scrollSecondary;
2047 | } else {
2048 | scrollX = scrollSecondary;
2049 | scrollY = scrollPrimary;
2050 | }
2051 | if (smooth) {
2052 | mBaseGridView.smoothScrollBy(scrollX, scrollY);
2053 | } else {
2054 | mBaseGridView.scrollBy(scrollX, scrollY);
2055 | }
2056 | }
2057 | }
2058 |
2059 | public void setPruneChild(boolean pruneChild) {
2060 | if (mPruneChild != pruneChild) {
2061 | mPruneChild = pruneChild;
2062 | if (mPruneChild) {
2063 | requestLayout();
2064 | }
2065 | }
2066 | }
2067 |
2068 | public boolean getPruneChild() {
2069 | return mPruneChild;
2070 | }
2071 |
2072 | private int findImmediateChildIndex(View view) {
2073 | while (view != null && view != mBaseGridView) {
2074 | int index = mBaseGridView.indexOfChild(view);
2075 | if (index >= 0) {
2076 | return index;
2077 | }
2078 | view = (View) view.getParent();
2079 | }
2080 | return NO_POSITION;
2081 | }
2082 |
2083 | void setFocusSearchDisabled(boolean disabled) {
2084 | mFocusSearchDisabled = disabled;
2085 | }
2086 |
2087 | boolean isFocusSearchDisabled() {
2088 | return mFocusSearchDisabled;
2089 | }
2090 |
2091 | @Override
2092 | public View onInterceptFocusSearch(View focused, int direction) {
2093 | if (mFocusSearchDisabled) {
2094 | return focused;
2095 | }
2096 | return null;
2097 | }
2098 |
2099 | boolean hasPreviousViewInSameRow(int pos) {
2100 | if (mGrid == null || pos == NO_POSITION) {
2101 | return false;
2102 | }
2103 | if (mFirstVisiblePos > 0) {
2104 | return true;
2105 | }
2106 | final int focusedRow = mGrid.getLocation(pos).row;
2107 | for (int i = getChildCount() - 1; i >= 0; i--) {
2108 | int position = getPositionByIndex(i);
2109 | StaggeredGrid.Location loc = mGrid.getLocation(position);
2110 | if (loc != null && loc.row == focusedRow) {
2111 | if (position < pos) {
2112 | return true;
2113 | }
2114 | }
2115 | }
2116 | return false;
2117 | }
2118 |
2119 | @Override
2120 | public boolean onAddFocusables(RecyclerView recyclerView,
2121 | ArrayList views, int direction, int focusableMode) {
2122 | if (mFocusSearchDisabled) {
2123 | return true;
2124 | }
2125 | // If this viewgroup or one of its children currently has focus then we
2126 | // consider our children for focus searching in main direction on the same row.
2127 | // If this viewgroup has no focus and using focus align, we want the system
2128 | // to ignore our children and pass focus to the viewgroup, which will pass
2129 | // focus on to its children appropriately.
2130 | // If this viewgroup has no focus and not using focus align, we want to
2131 | // consider the child that does not overlap with padding area.
2132 | if (recyclerView.hasFocus()) {
2133 | final int movement = getMovement(direction);
2134 | if (movement != PREV_ITEM && movement != NEXT_ITEM) {
2135 | // Move on secondary direction uses default addFocusables().
2136 | return false;
2137 | }
2138 | final View focused = recyclerView.findFocus();
2139 | final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused));
2140 | // Add focusables of focused item.
2141 | if (focusedPos != NO_POSITION) {
2142 | findViewByPosition(focusedPos).addFocusables(views, direction, focusableMode);
2143 | }
2144 | final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
2145 | mGrid.getLocation(focusedPos).row : NO_POSITION;
2146 | // Add focusables of next neighbor of same row on the focus search direction.
2147 | if (mGrid != null) {
2148 | final int focusableCount = views.size();
2149 | for (int i = 0, count = getChildCount(); i < count; i++) {
2150 | int index = movement == NEXT_ITEM ? i : count - 1 - i;
2151 | final View child = getChildAt(index);
2152 | if (child.getVisibility() != View.VISIBLE) {
2153 | continue;
2154 | }
2155 | int position = getPositionByIndex(index);
2156 | StaggeredGrid.Location loc = mGrid.getLocation(position);
2157 | if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) {
2158 | if (focusedPos == NO_POSITION ||
2159 | (movement == NEXT_ITEM && position > focusedPos)
2160 | || (movement == PREV_ITEM && position < focusedPos)) {
2161 | child.addFocusables(views, direction, focusableMode);
2162 | if (views.size() > focusableCount) {
2163 | break;
2164 | }
2165 | }
2166 | }
2167 | }
2168 | }
2169 | } else {
2170 | if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
2171 | // adding views not overlapping padding area to avoid scrolling in gaining focus
2172 | int left = mWindowAlignment.mainAxis().getPaddingLow();
2173 | int right = mWindowAlignment.mainAxis().getClientSize() + left;
2174 | int focusableCount = views.size();
2175 | for (int i = 0, count = getChildCount(); i < count; i++) {
2176 | View child = getChildAt(i);
2177 | if (child.getVisibility() == View.VISIBLE) {
2178 | if (getViewMin(child) >= left && getViewMax(child) <= right) {
2179 | child.addFocusables(views, direction, focusableMode);
2180 | }
2181 | }
2182 | }
2183 | // if we cannot find any, then just add all children.
2184 | if (views.size() == focusableCount) {
2185 | for (int i = 0, count = getChildCount(); i < count; i++) {
2186 | View child = getChildAt(i);
2187 | if (child.getVisibility() == View.VISIBLE) {
2188 | child.addFocusables(views, direction, focusableMode);
2189 | }
2190 | }
2191 | if (views.size() != focusableCount) {
2192 | return true;
2193 | }
2194 | } else {
2195 | return true;
2196 | }
2197 | // if still cannot find any, fall through and add itself
2198 | }
2199 | if (recyclerView.isFocusable()) {
2200 | views.add(recyclerView);
2201 | }
2202 | }
2203 | return true;
2204 | }
2205 |
2206 | @Override
2207 | public View onFocusSearchFailed(View focused, int direction, Recycler recycler,
2208 | State state) {
2209 | if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
2210 |
2211 | saveContext(recycler, state);
2212 | View view = null;
2213 | int movement = getMovement(direction);
2214 | final FocusFinder ff = FocusFinder.getInstance();
2215 | if (movement == NEXT_ITEM) {
2216 | while (view == null && !appendOneVisibleItem()) {
2217 | view = ff.findNextFocus(mBaseGridView, focused, direction);
2218 | }
2219 | } else if (movement == PREV_ITEM){
2220 | while (view == null && !prependOneVisibleItem()) {
2221 | view = ff.findNextFocus(mBaseGridView, focused, direction);
2222 | }
2223 | }
2224 | if (view == null) {
2225 | // returning the same view to prevent focus lost when scrolling past the end of the list
2226 | if (movement == PREV_ITEM) {
2227 | view = mFocusOutFront ? null : focused;
2228 | } else if (movement == NEXT_ITEM){
2229 | view = mFocusOutEnd ? null : focused;
2230 | }
2231 | }
2232 | leaveContext();
2233 | if (DEBUG) Log.v(getTag(), "returning view " + view);
2234 | return view;
2235 | }
2236 |
2237 | boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
2238 | Rect previouslyFocusedRect) {
2239 | switch (mFocusScrollStrategy) {
2240 | case BaseGridView.FOCUS_SCROLL_ALIGNED:
2241 | default:
2242 | return gridOnRequestFocusInDescendantsAligned(recyclerView,
2243 | direction, previouslyFocusedRect);
2244 | case BaseGridView.FOCUS_SCROLL_PAGE:
2245 | case BaseGridView.FOCUS_SCROLL_ITEM:
2246 | return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
2247 | direction, previouslyFocusedRect);
2248 | }
2249 | }
2250 |
2251 | private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView,
2252 | int direction, Rect previouslyFocusedRect) {
2253 | View view = findViewByPosition(mFocusPosition);
2254 | if (view != null) {
2255 | boolean result = view.requestFocus(direction, previouslyFocusedRect);
2256 | if (!result && DEBUG) {
2257 | Log.w(getTag(), "failed to request focus on " + view);
2258 | }
2259 | return result;
2260 | }
2261 | return false;
2262 | }
2263 |
2264 | private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView,
2265 | int direction, Rect previouslyFocusedRect) {
2266 | // focus to view not overlapping padding area to avoid scrolling in gaining focus
2267 | int index;
2268 | int increment;
2269 | int end;
2270 | int count = getChildCount();
2271 | if ((direction & View.FOCUS_FORWARD) != 0) {
2272 | index = 0;
2273 | increment = 1;
2274 | end = count;
2275 | } else {
2276 | index = count - 1;
2277 | increment = -1;
2278 | end = -1;
2279 | }
2280 | int left = mWindowAlignment.mainAxis().getPaddingLow();
2281 | int right = mWindowAlignment.mainAxis().getClientSize() + left;
2282 | for (int i = index; i != end; i += increment) {
2283 | View child = getChildAt(i);
2284 | if (child.getVisibility() == View.VISIBLE) {
2285 | if (getViewMin(child) >= left && getViewMax(child) <= right) {
2286 | if (child.requestFocus(direction, previouslyFocusedRect)) {
2287 | return true;
2288 | }
2289 | }
2290 | }
2291 | }
2292 | return false;
2293 | }
2294 |
2295 | private final static int PREV_ITEM = 0;
2296 | private final static int NEXT_ITEM = 1;
2297 | private final static int PREV_ROW = 2;
2298 | private final static int NEXT_ROW = 3;
2299 |
2300 | private int getMovement(int direction) {
2301 | int movement = View.FOCUS_LEFT;
2302 |
2303 | if (mOrientation == HORIZONTAL) {
2304 | switch(direction) {
2305 | case View.FOCUS_LEFT:
2306 | movement = PREV_ITEM;
2307 | break;
2308 | case View.FOCUS_RIGHT:
2309 | movement = NEXT_ITEM;
2310 | break;
2311 | case View.FOCUS_UP:
2312 | movement = PREV_ROW;
2313 | break;
2314 | case View.FOCUS_DOWN:
2315 | movement = NEXT_ROW;
2316 | break;
2317 | }
2318 | } else if (mOrientation == VERTICAL) {
2319 | switch(direction) {
2320 | case View.FOCUS_LEFT:
2321 | movement = PREV_ROW;
2322 | break;
2323 | case View.FOCUS_RIGHT:
2324 | movement = NEXT_ROW;
2325 | break;
2326 | case View.FOCUS_UP:
2327 | movement = PREV_ITEM;
2328 | break;
2329 | case View.FOCUS_DOWN:
2330 | movement = NEXT_ITEM;
2331 | break;
2332 | }
2333 | }
2334 |
2335 | return movement;
2336 | }
2337 |
2338 | int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
2339 | View view = findViewByPosition(mFocusPosition);
2340 | if (view == null) {
2341 | return i;
2342 | }
2343 | int focusIndex = recyclerView.indexOfChild(view);
2344 | // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
2345 | // drawing order is 0 1 2 3 9 8 7 6 5 4
2346 | if (i < focusIndex) {
2347 | return i;
2348 | } else if (i < childCount - 1) {
2349 | return focusIndex + childCount - 1 - i;
2350 | } else {
2351 | return focusIndex;
2352 | }
2353 | }
2354 |
2355 | @Override
2356 | public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
2357 | RecyclerView.Adapter newAdapter) {
2358 | discardLayoutInfo();
2359 | mFocusPosition = NO_POSITION;
2360 | super.onAdapterChanged(oldAdapter, newAdapter);
2361 | }
2362 |
2363 | private void discardLayoutInfo() {
2364 | mGrid = null;
2365 | mRows = null;
2366 | mRowSizeSecondary = null;
2367 | mFirstVisiblePos = -1;
2368 | mLastVisiblePos = -1;
2369 | mRowSecondarySizeRefresh = false;
2370 | }
2371 |
2372 | public void setLayoutEnabled(boolean layoutEnabled) {
2373 | if (mLayoutEnabled != layoutEnabled) {
2374 | mLayoutEnabled = layoutEnabled;
2375 | requestLayout();
2376 | }
2377 | }
2378 | }
2379 |
--------------------------------------------------------------------------------
/HorizontalGridView/src/com/opensource/widget/HorizontalGridView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project.
3 | *
4 | * yinglovezhuzhu@gmail.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package com.opensource.widget;
19 |
20 | import android.content.Context;
21 | import android.content.res.TypedArray;
22 | import android.graphics.Bitmap;
23 | import android.graphics.Canvas;
24 | import android.graphics.Color;
25 | import android.graphics.LinearGradient;
26 | import android.graphics.Paint;
27 | import android.graphics.PorterDuff;
28 | import android.graphics.PorterDuffXfermode;
29 | import android.graphics.Rect;
30 | import android.graphics.Shader;
31 | import android.util.AttributeSet;
32 | import android.util.TypedValue;
33 | import android.view.View;
34 |
35 | /**
36 | * A view that shows items in a horizontal scrolling list. The items come from
37 | * the {@link com.opensource.widget.RecyclerView.Adapter} associated with this view.
38 | */
39 | public class HorizontalGridView extends BaseGridView {
40 |
41 | private boolean mFadingLowEdge;
42 | private boolean mFadingHighEdge;
43 |
44 | private Paint mTempPaint = new Paint();
45 | private Bitmap mTempBitmapLow;
46 | private LinearGradient mLowFadeShader;
47 | private int mLowFadeShaderLength;
48 | private int mLowFadeShaderOffset;
49 | private Bitmap mTempBitmapHigh;
50 | private LinearGradient mHighFadeShader;
51 | private int mHighFadeShaderLength;
52 | private int mHighFadeShaderOffset;
53 | private Rect mTempRect = new Rect();
54 |
55 | public HorizontalGridView(Context context) {
56 | this(context, null);
57 | }
58 |
59 | public HorizontalGridView(Context context, AttributeSet attrs) {
60 | this(context, attrs, 0);
61 | }
62 |
63 | public HorizontalGridView(Context context, AttributeSet attrs, int defStyle) {
64 | super(context, attrs, defStyle);
65 | mLayoutManager.setOrientation(RecyclerView.HORIZONTAL);
66 | initAttributes(context, attrs);
67 | }
68 |
69 | protected void initAttributes(Context context, AttributeSet attrs) {
70 | initBaseGridViewAttributes(context, attrs);
71 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbHorizontalGridView);
72 | setRowHeight(a);
73 | setNumRows(a.getInt(R.styleable.lbHorizontalGridView_numberOfRows, 1));
74 | a.recycle();
75 | setWillNotDraw(false);
76 | mTempPaint = new Paint();
77 | mTempPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
78 | }
79 |
80 | void setRowHeight(TypedArray array) {
81 | TypedValue typedValue = array.peekValue(R.styleable.lbHorizontalGridView_rowHeight);
82 | int size;
83 | if (typedValue != null && typedValue.type == TypedValue.TYPE_DIMENSION) {
84 | size = array.getDimensionPixelSize(R.styleable.lbHorizontalGridView_rowHeight, 0);
85 | } else {
86 | size = array.getInt(R.styleable.lbHorizontalGridView_rowHeight, 0);
87 | }
88 | setRowHeight(size);
89 | }
90 |
91 | /**
92 | * Set the number of rows. Defaults to one.
93 | */
94 | public void setNumRows(int numRows) {
95 | mLayoutManager.setNumRows(numRows);
96 | requestLayout();
97 | }
98 |
99 | /**
100 | * Set the row height.
101 | *
102 | * @param height May be WRAP_CONTENT, or a size in pixels. If zero,
103 | * row height will be fixed based on number of rows and view height.
104 | */
105 | public void setRowHeight(int height) {
106 | mLayoutManager.setRowHeight(height);
107 | requestLayout();
108 | }
109 |
110 | /**
111 | * Set fade out left edge to transparent. Note turn on fading edge is very expensive
112 | * that you should turn off when HorizontalGridView is scrolling.
113 | */
114 | public final void setFadingLeftEdge(boolean fading) {
115 | if (mFadingLowEdge != fading) {
116 | mFadingLowEdge = fading;
117 | if (!mFadingLowEdge) {
118 | mTempBitmapLow = null;
119 | }
120 | invalidate();
121 | }
122 | }
123 |
124 | /**
125 | * Return true if fading left edge.
126 | */
127 | public final boolean getFadingLeftEdge() {
128 | return mFadingLowEdge;
129 | }
130 |
131 | /**
132 | * Set left edge fading length in pixels.
133 | */
134 | public final void setFadingLeftEdgeLength(int fadeLength) {
135 | if (mLowFadeShaderLength != fadeLength) {
136 | mLowFadeShaderLength = fadeLength;
137 | if (mLowFadeShaderLength != 0) {
138 | mLowFadeShader = new LinearGradient(0, 0, mLowFadeShaderLength, 0,
139 | Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP);
140 | } else {
141 | mLowFadeShader = null;
142 | }
143 | invalidate();
144 | }
145 | }
146 |
147 | /**
148 | * Get left edge fading length in pixels.
149 | */
150 | public final int getFadingLeftEdgeLength() {
151 | return mLowFadeShaderLength;
152 | }
153 |
154 | /**
155 | * Set distance in pixels between fading start position and left padding edge.
156 | * The fading start position is positive when start position is inside left padding
157 | * area. Default value is 0, means that the fading starts from left padding edge.
158 | */
159 | public final void setFadingLeftEdgeOffset(int fadeOffset) {
160 | if (mLowFadeShaderOffset != fadeOffset) {
161 | mLowFadeShaderOffset = fadeOffset;
162 | invalidate();
163 | }
164 | }
165 |
166 | /**
167 | * Get distance in pixels between fading start position and left padding edge.
168 | * The fading start position is positive when start position is inside left padding
169 | * area. Default value is 0, means that the fading starts from left padding edge.
170 | */
171 | public final int getFadingLeftEdgeOffset() {
172 | return mLowFadeShaderOffset;
173 | }
174 |
175 | /**
176 | * Set fade out right edge to transparent. Note turn on fading edge is very expensive
177 | * that you should turn off when HorizontalGridView is scrolling.
178 | */
179 | public final void setFadingRightEdge(boolean fading) {
180 | if (mFadingHighEdge != fading) {
181 | mFadingHighEdge = fading;
182 | if (!mFadingHighEdge) {
183 | mTempBitmapHigh = null;
184 | }
185 | invalidate();
186 | }
187 | }
188 |
189 | /**
190 | * Return true if fading right edge.
191 | */
192 | public final boolean getFadingRightEdge() {
193 | return mFadingHighEdge;
194 | }
195 |
196 | /**
197 | * Set right edge fading length in pixels.
198 | */
199 | public final void setFadingRightEdgeLength(int fadeLength) {
200 | if (mHighFadeShaderLength != fadeLength) {
201 | mHighFadeShaderLength = fadeLength;
202 | if (mHighFadeShaderLength != 0) {
203 | mHighFadeShader = new LinearGradient(0, 0, mHighFadeShaderLength, 0,
204 | Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP);
205 | } else {
206 | mHighFadeShader = null;
207 | }
208 | invalidate();
209 | }
210 | }
211 |
212 | /**
213 | * Get right edge fading length in pixels.
214 | */
215 | public final int getFadingRightEdgeLength() {
216 | return mHighFadeShaderLength;
217 | }
218 |
219 | /**
220 | * Get distance in pixels between fading start position and right padding edge.
221 | * The fading start position is positive when start position is inside right padding
222 | * area. Default value is 0, means that the fading starts from right padding edge.
223 | */
224 | public final void setFadingRightEdgeOffset(int fadeOffset) {
225 | if (mHighFadeShaderOffset != fadeOffset) {
226 | mHighFadeShaderOffset = fadeOffset;
227 | invalidate();
228 | }
229 | }
230 |
231 | /**
232 | * Set distance in pixels between fading start position and right padding edge.
233 | * The fading start position is positive when start position is inside right padding
234 | * area. Default value is 0, means that the fading starts from right padding edge.
235 | */
236 | public final int getFadingRightEdgeOffset() {
237 | return mHighFadeShaderOffset;
238 | }
239 |
240 | private boolean needsFadingLowEdge() {
241 | if (!mFadingLowEdge) {
242 | return false;
243 | }
244 | final int c = getChildCount();
245 | for (int i = 0; i < c; i++) {
246 | View view = getChildAt(i);
247 | if (mLayoutManager.getOpticalLeft(view) <
248 | getPaddingLeft() - mLowFadeShaderOffset) {
249 | return true;
250 | }
251 | }
252 | return false;
253 | }
254 |
255 | private boolean needsFadingHighEdge() {
256 | if (!mFadingHighEdge) {
257 | return false;
258 | }
259 | final int c = getChildCount();
260 | for (int i = c - 1; i >= 0; i--) {
261 | View view = getChildAt(i);
262 | if (mLayoutManager.getOpticalRight(view) > getWidth()
263 | - getPaddingRight() + mHighFadeShaderOffset) {
264 | return true;
265 | }
266 | }
267 | return false;
268 | }
269 |
270 | private Bitmap getTempBitmapLow() {
271 | if (mTempBitmapLow == null
272 | || mTempBitmapLow.getWidth() != mLowFadeShaderLength
273 | || mTempBitmapLow.getHeight() != getHeight()) {
274 | mTempBitmapLow = Bitmap.createBitmap(mLowFadeShaderLength, getHeight(),
275 | Bitmap.Config.ARGB_8888);
276 | }
277 | return mTempBitmapLow;
278 | }
279 |
280 | private Bitmap getTempBitmapHigh() {
281 | if (mTempBitmapHigh == null
282 | || mTempBitmapHigh.getWidth() != mHighFadeShaderLength
283 | || mTempBitmapHigh.getHeight() != getHeight()) {
284 | // TODO: fix logic for sharing mTempBitmapLow
285 | if (false && mTempBitmapLow != null
286 | && mTempBitmapLow.getWidth() == mHighFadeShaderLength
287 | && mTempBitmapLow.getHeight() == getHeight()) {
288 | // share same bitmap for low edge fading and high edge fading.
289 | mTempBitmapHigh = mTempBitmapLow;
290 | } else {
291 | mTempBitmapHigh = Bitmap.createBitmap(mHighFadeShaderLength, getHeight(),
292 | Bitmap.Config.ARGB_8888);
293 | }
294 | }
295 | return mTempBitmapHigh;
296 | }
297 |
298 | @Override
299 | public void draw(Canvas canvas) {
300 | final boolean needsFadingLow = needsFadingLowEdge();
301 | final boolean needsFadingHigh = needsFadingHighEdge();
302 | if (!needsFadingLow) {
303 | mTempBitmapLow = null;
304 | }
305 | if (!needsFadingHigh) {
306 | mTempBitmapHigh = null;
307 | }
308 | if (!needsFadingLow && !needsFadingHigh) {
309 | super.draw(canvas);
310 | return;
311 | }
312 |
313 | int lowEdge = mFadingLowEdge? getPaddingLeft() - mLowFadeShaderOffset - mLowFadeShaderLength : 0;
314 | int highEdge = mFadingHighEdge ? getWidth() - getPaddingRight()
315 | + mHighFadeShaderOffset + mHighFadeShaderLength : getWidth();
316 |
317 | // draw not-fade content
318 | int save = canvas.save();
319 | canvas.clipRect(lowEdge + (mFadingLowEdge ? mLowFadeShaderLength : 0), 0,
320 | highEdge - (mFadingHighEdge ? mHighFadeShaderLength : 0), getHeight());
321 | super.draw(canvas);
322 | canvas.restoreToCount(save);
323 |
324 | Canvas tmpCanvas = new Canvas();
325 | mTempRect.top = 0;
326 | mTempRect.bottom = getHeight();
327 | if (needsFadingLow && mLowFadeShaderLength > 0) {
328 | Bitmap tempBitmap = getTempBitmapLow();
329 | tempBitmap.eraseColor(Color.TRANSPARENT);
330 | tmpCanvas.setBitmap(tempBitmap);
331 | // draw original content
332 | int tmpSave = tmpCanvas.save();
333 | tmpCanvas.clipRect(0, 0, mLowFadeShaderLength, getHeight());
334 | tmpCanvas.translate(-lowEdge, 0);
335 | super.draw(tmpCanvas);
336 | tmpCanvas.restoreToCount(tmpSave);
337 | // draw fading out
338 | mTempPaint.setShader(mLowFadeShader);
339 | tmpCanvas.drawRect(0, 0, mLowFadeShaderLength, getHeight(), mTempPaint);
340 | // copy back to canvas
341 | mTempRect.left = 0;
342 | mTempRect.right = mLowFadeShaderLength;
343 | canvas.translate(lowEdge, 0);
344 | canvas.drawBitmap(tempBitmap, mTempRect, mTempRect, null);
345 | canvas.translate(-lowEdge, 0);
346 | }
347 | if (needsFadingHigh && mHighFadeShaderLength > 0) {
348 | Bitmap tempBitmap = getTempBitmapHigh();
349 | tempBitmap.eraseColor(Color.TRANSPARENT);
350 | tmpCanvas.setBitmap(tempBitmap);
351 | // draw original content
352 | int tmpSave = tmpCanvas.save();
353 | tmpCanvas.clipRect(0, 0, mHighFadeShaderLength, getHeight());
354 | tmpCanvas.translate(-(highEdge - mHighFadeShaderLength), 0);
355 | super.draw(tmpCanvas);
356 | tmpCanvas.restoreToCount(tmpSave);
357 | // draw fading out
358 | mTempPaint.setShader(mHighFadeShader);
359 | tmpCanvas.drawRect(0, 0, mHighFadeShaderLength, getHeight(), mTempPaint);
360 | // copy back to canvas
361 | mTempRect.left = 0;
362 | mTempRect.right = mHighFadeShaderLength;
363 | canvas.translate(highEdge - mHighFadeShaderLength, 0);
364 | canvas.drawBitmap(tempBitmap, mTempRect, mTempRect, null);
365 | canvas.translate(-(highEdge - mHighFadeShaderLength), 0);
366 | }
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/HorizontalGridView/src/com/opensource/widget/ItemAlignment.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project.
3 | *
4 | * yinglovezhuzhu@gmail.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | package com.opensource.widget;
20 |
21 | import android.graphics.Rect;
22 | import android.view.View;
23 | import android.view.ViewGroup;
24 |
25 | import com.opensource.widget.GridLayoutManager.LayoutParams;
26 |
27 | import static com.opensource.widget.BaseGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED;
28 | import static com.opensource.widget.RecyclerView.HORIZONTAL;
29 | import static com.opensource.widget.RecyclerView.VERTICAL;
30 |
31 | /**
32 | * Defines alignment position on two directions of an item view. Typically item
33 | * view alignment is at the center of the view. The class allows defining
34 | * alignment at left/right or fixed offset/percentage position; it also allows
35 | * using descendant view by id match.
36 | */
37 | class ItemAlignment {
38 |
39 | final static class Axis {
40 | private int mOrientation;
41 | private int mOffset = 0;
42 | private float mOffsetPercent = 50;
43 | private int mViewId = 0;
44 | private boolean mOffsetWithPadding = false;
45 | private Rect mRect = new Rect();
46 |
47 | Axis(int orientation) {
48 | mOrientation = orientation;
49 | }
50 |
51 | public void setItemAlignmentOffset(int offset) {
52 | mOffset = offset;
53 | }
54 |
55 | public int getItemAlignmentOffset() {
56 | return mOffset;
57 | }
58 |
59 | public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
60 | mOffsetWithPadding = withPadding;
61 | }
62 |
63 | public boolean isItemAlignmentOffsetWithPadding() {
64 | return mOffsetWithPadding;
65 | }
66 |
67 | public void setItemAlignmentOffsetPercent(float percent) {
68 | if ( (percent < 0 || percent > 100) &&
69 | percent != ITEM_ALIGN_OFFSET_PERCENT_DISABLED) {
70 | throw new IllegalArgumentException();
71 | }
72 | mOffsetPercent = percent;
73 | }
74 |
75 | public float getItemAlignmentOffsetPercent() {
76 | return mOffsetPercent;
77 | }
78 |
79 | public void setItemAlignmentViewId(int viewId) {
80 | mViewId = viewId;
81 | }
82 |
83 | public int getItemAlignmentViewId() {
84 | return mViewId;
85 | }
86 |
87 | /**
88 | * get alignment position relative to optical left/top of itemView.
89 | */
90 | public int getAlignmentPosition(View itemView) {
91 | LayoutParams p = (LayoutParams) itemView.getLayoutParams();
92 | View view = itemView;
93 | if (mViewId != 0) {
94 | view = itemView.findViewById(mViewId);
95 | if (view == null) {
96 | view = itemView;
97 | }
98 | }
99 | int alignPos;
100 | if (mOrientation == HORIZONTAL) {
101 | if (mOffset >= 0) {
102 | alignPos = mOffset;
103 | if (mOffsetWithPadding) {
104 | alignPos += view.getPaddingLeft();
105 | }
106 | } else {
107 | alignPos = view == itemView ? p.getOpticalWidth(view) : view.getWidth()
108 | + mOffset;
109 | if (mOffsetWithPadding) {
110 | alignPos -= view.getPaddingRight();
111 | }
112 | }
113 | if (mOffsetPercent != ITEM_ALIGN_OFFSET_PERCENT_DISABLED) {
114 | alignPos += ((view == itemView ? p.getOpticalWidth(view) : view.getWidth())
115 | * mOffsetPercent) / 100f;
116 | }
117 | if (itemView != view) {
118 | mRect.left = alignPos;
119 | ((ViewGroup) itemView).offsetDescendantRectToMyCoords(view, mRect);
120 | alignPos = mRect.left - p.getOpticalLeftInset();
121 | }
122 | } else {
123 | if (mOffset >= 0) {
124 | alignPos = mOffset;
125 | if (mOffsetWithPadding) {
126 | alignPos += view.getPaddingTop();
127 | }
128 | } else {
129 | alignPos = view == itemView ? p.getOpticalHeight(view) : view.getHeight()
130 | + mOffset;
131 | if (mOffsetWithPadding) {
132 | alignPos += view.getPaddingBottom();
133 | }
134 | }
135 | if (mOffsetPercent != ITEM_ALIGN_OFFSET_PERCENT_DISABLED) {
136 | alignPos += ((view == itemView ? p.getOpticalHeight(view) : view.getHeight())
137 | * mOffsetPercent) / 100f;
138 | }
139 | if (itemView != view) {
140 | mRect.top = alignPos;
141 | ((ViewGroup) itemView).offsetDescendantRectToMyCoords(view, mRect);
142 | alignPos = mRect.top - p.getOpticalTopInset();
143 | }
144 | }
145 | return alignPos;
146 | }
147 | }
148 |
149 | private int mOrientation = HORIZONTAL;
150 |
151 | final public Axis vertical = new Axis(VERTICAL);
152 |
153 | final public Axis horizontal = new Axis(HORIZONTAL);
154 |
155 | private Axis mMainAxis = horizontal;
156 |
157 | private Axis mSecondAxis = vertical;
158 |
159 | final public Axis mainAxis() {
160 | return mMainAxis;
161 | }
162 |
163 | final public Axis secondAxis() {
164 | return mSecondAxis;
165 | }
166 |
167 | final public void setOrientation(int orientation) {
168 | mOrientation = orientation;
169 | if (mOrientation == HORIZONTAL) {
170 | mMainAxis = horizontal;
171 | mSecondAxis = vertical;
172 | } else {
173 | mMainAxis = vertical;
174 | mSecondAxis = horizontal;
175 | }
176 | }
177 |
178 | final public int getOrientation() {
179 | return mOrientation;
180 | }
181 |
182 |
183 | }
184 |
--------------------------------------------------------------------------------
/HorizontalGridView/src/com/opensource/widget/LinearSmoothScroller.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project.
3 | *
4 | * yinglovezhuzhu@gmail.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | package com.opensource.widget;
20 |
21 | import android.content.Context;
22 | import android.graphics.PointF;
23 | import android.util.DisplayMetrics;
24 | import android.util.Log;
25 | import android.view.View;
26 | import android.view.animation.DecelerateInterpolator;
27 | import android.view.animation.LinearInterpolator;
28 |
29 | /**
30 | * {@link RecyclerView.SmoothScroller} implementation which uses
31 | * {@link android.view.animation.LinearInterpolator} until the target position becames a child of
32 | * the RecyclerView and then uses
33 | * {@link android.view.animation.DecelerateInterpolator} to slowly approach to target position.
34 | */
35 | abstract public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
36 |
37 | private static final String TAG = "LinearSmoothScroller";
38 |
39 | private static final boolean DEBUG = false;
40 |
41 | private static final float MILLISECONDS_PER_INCH = 25f;
42 |
43 | private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
44 |
45 | /**
46 | * Align child view's left or top with parent view's left or top
47 | *
48 | * @see #calculateDtToFit(int, int, int, int, int)
49 | * @see #calculateDxToMakeVisible(android.view.View, int)
50 | * @see #calculateDyToMakeVisible(android.view.View, int)
51 | */
52 | public static final int SNAP_TO_START = -1;
53 |
54 | /**
55 | * Align child view's right or bottom with parent view's right or bottom
56 | *
57 | * @see #calculateDtToFit(int, int, int, int, int)
58 | * @see #calculateDxToMakeVisible(android.view.View, int)
59 | * @see #calculateDyToMakeVisible(android.view.View, int)
60 | */
61 | public static final int SNAP_TO_END = 1;
62 |
63 | /**
64 | * Decides if the child should be snapped from start or end, depending on where it
65 | * currently is in relation to its parent.
66 | * For instance, if the view is virtually on the left of RecyclerView, using
67 | * {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}
68 | *
69 | * @see #calculateDtToFit(int, int, int, int, int)
70 | * @see #calculateDxToMakeVisible(android.view.View, int)
71 | * @see #calculateDyToMakeVisible(android.view.View, int)
72 | */
73 | public static final int SNAP_TO_ANY = 0;
74 |
75 | // Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target
76 | // view is not laid out until interim target position is reached, we can detect the case before
77 | // scrolling slows down and reschedule another interim target scroll
78 | private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f;
79 |
80 | protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator();
81 |
82 | protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
83 |
84 | protected PointF mTargetVector;
85 |
86 | private final float MILLISECONDS_PER_PX;
87 |
88 | // Temporary variables to keep track of the interim scroll target. These values do not
89 | // point to a real item position, rather point to an estimated location pixels.
90 | protected int mInterimTargetDx = 0, mInterimTargetDy = 0;
91 |
92 | public LinearSmoothScroller(Context context) {
93 | MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
94 | }
95 |
96 | /**
97 | * {@inheritDoc}
98 | */
99 | @Override
100 | protected void onStart() {
101 |
102 | }
103 |
104 | /**
105 | * {@inheritDoc}
106 | */
107 | @Override
108 | protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
109 | final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
110 | final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
111 | final int distance = (int) Math.sqrt(dx * dx + dy * dy);
112 | final int time = calculateTimeForDeceleration(distance);
113 | if (time > 0) {
114 | action.update(-dx, -dy, time, mDecelerateInterpolator);
115 | }
116 | }
117 |
118 | /**
119 | * {@inheritDoc}
120 | */
121 | @Override
122 | protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
123 | if (getChildCount() == 0) {
124 | stop();
125 | return;
126 | }
127 | if (DEBUG && mTargetVector != null
128 | && ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
129 | throw new IllegalStateException("Scroll happened in the opposite direction"
130 | + " of the target. Some calculations are wrong");
131 | }
132 | mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
133 | mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);
134 |
135 | if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
136 | updateActionForInterimTarget(action);
137 | } // everything is valid, keep going
138 |
139 | }
140 |
141 | /**
142 | * {@inheritDoc}
143 | */
144 | @Override
145 | protected void onStop() {
146 | mInterimTargetDx = mInterimTargetDy = 0;
147 | mTargetVector = null;
148 | }
149 |
150 | /**
151 | * Calculates the scroll speed.
152 | *
153 | * @param displayMetrics DisplayMetrics to be used for real dimension calculations
154 | * @return The time (in ms) it should take for each pixel. For instance, if returned value is
155 | * 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
156 | */
157 | protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
158 | return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
159 | }
160 |
161 | /**
162 | * Calculates the time for deceleration so that transition from LinearInterpolator to
163 | * DecelerateInterpolator looks smooth.
164 | *
165 | * @param dx Distance to scroll
166 | * @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
167 | * from LinearInterpolation
168 | */
169 | protected int calculateTimeForDeceleration(int dx) {
170 | // we want to cover same area with the linear interpolator for the first 10% of the
171 | // interpolation. After that, deceleration will take control.
172 | // area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
173 | // which gives 0.100028 when x = .3356
174 | // this is why we divide linear scrolling time with .3356
175 | return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
176 | }
177 |
178 | /**
179 | * Calculates the time it should take to scroll the given distance (in pixels)
180 | *
181 | * @param dx Distance in pixels that we want to scroll
182 | * @return Time in milliseconds
183 | * @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
184 | */
185 | protected int calculateTimeForScrolling(int dx) {
186 | // In a case where dx is very small, rounding may return 0 although dx > 0.
187 | // To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
188 | // time.
189 | return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
190 | }
191 |
192 | /**
193 | * When scrolling towards a child view, this method defines whether we should align the left
194 | * or the right edge of the child with the parent RecyclerView.
195 | *
196 | * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
197 | * @see #SNAP_TO_START
198 | * @see #SNAP_TO_END
199 | * @see #SNAP_TO_ANY
200 | */
201 | protected int getHorizontalSnapPreference() {
202 | return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
203 | mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
204 | }
205 |
206 | /**
207 | * When scrolling towards a child view, this method defines whether we should align the top
208 | * or the bottom edge of the child with the parent RecyclerView.
209 | *
210 | * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
211 | * @see #SNAP_TO_START
212 | * @see #SNAP_TO_END
213 | * @see #SNAP_TO_ANY
214 | */
215 | protected int getVerticalSnapPreference() {
216 | return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
217 | mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
218 | }
219 |
220 | /**
221 | * When the target scroll position is not a child of the RecyclerView, this method calculates
222 | * a direction vector towards that child and triggers a smooth scroll.
223 | *
224 | * @see #computeScrollVectorForPosition(int)
225 | */
226 | protected void updateActionForInterimTarget(Action action) {
227 | // find an interim target position
228 | PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
229 | if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
230 | Log.e(TAG, "To support smooth scrolling, you should override \n"
231 | + "LayoutManager#computeScrollVectorForPosition.\n"
232 | + "Falling back to instant scroll");
233 | final int target = getTargetPosition();
234 | stop();
235 | instantScrollToPosition(target);
236 | return;
237 | }
238 | normalize(scrollVector);
239 | mTargetVector = scrollVector;
240 |
241 | mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
242 | mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
243 | final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
244 | // To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
245 | // interim target. Since we track the distance travelled in onSeekTargetStep callback, it
246 | // won't actually scroll more than what we need.
247 | action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO)
248 | , (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO)
249 | , (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
250 | }
251 |
252 | private int clampApplyScroll(int tmpDt, int dt) {
253 | final int before = tmpDt;
254 | tmpDt -= dt;
255 | if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
256 | return 0;
257 | }
258 | return tmpDt;
259 | }
260 |
261 | /**
262 | * Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
263 | * {@link #calculateDyToMakeVisible(android.view.View, int)}
264 | */
265 | public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
266 | snapPreference) {
267 | switch (snapPreference) {
268 | case SNAP_TO_START:
269 | return boxStart - viewStart;
270 | case SNAP_TO_END:
271 | return boxEnd - viewEnd;
272 | case SNAP_TO_ANY:
273 | final int dtStart = boxStart - viewStart;
274 | if (dtStart > 0) {
275 | return dtStart;
276 | }
277 | final int dtEnd = boxEnd - viewEnd;
278 | if (dtEnd < 0) {
279 | return dtEnd;
280 | }
281 | break;
282 | default:
283 | throw new IllegalArgumentException("snap preference should be one of the"
284 | + " constants defined in SmoothScroller, starting with SNAP_");
285 | }
286 | return 0;
287 | }
288 |
289 | /**
290 | * Calculates the vertical scroll amount necessary to make the given view fully visible
291 | * inside the RecyclerView.
292 | *
293 | * @param view The view which we want to make fully visible
294 | * @param snapPreference The edge which the view should snap to when entering the visible
295 | * area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
296 | * {@link #SNAP_TO_END}.
297 | * @return The vertical scroll amount necessary to make the view visible with the given
298 | * snap preference.
299 | */
300 | public int calculateDyToMakeVisible(View view, int snapPreference) {
301 | final RecyclerView.LayoutManager layoutManager = getLayoutManager();
302 | if (!layoutManager.canScrollVertically()) {
303 | return 0;
304 | }
305 | final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
306 | view.getLayoutParams();
307 | final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
308 | final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
309 | final int start = layoutManager.getPaddingTop();
310 | final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
311 | return calculateDtToFit(top, bottom, start, end, snapPreference);
312 | }
313 |
314 | /**
315 | * Calculates the horizontal scroll amount necessary to make the given view fully visible
316 | * inside the RecyclerView.
317 | *
318 | * @param view The view which we want to make fully visible
319 | * @param snapPreference The edge which the view should snap to when entering the visible
320 | * area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
321 | * {@link #SNAP_TO_END}
322 | * @return The vertical scroll amount necessary to make the view visible with the given
323 | * snap preference.
324 | */
325 | public int calculateDxToMakeVisible(View view, int snapPreference) {
326 | final RecyclerView.LayoutManager layoutManager = getLayoutManager();
327 | if (!layoutManager.canScrollHorizontally()) {
328 | return 0;
329 | }
330 | final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
331 | view.getLayoutParams();
332 | final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
333 | final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
334 | final int start = layoutManager.getPaddingLeft();
335 | final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
336 | return calculateDtToFit(left, right, start, end, snapPreference);
337 | }
338 |
339 | abstract public PointF computeScrollVectorForPosition(int targetPosition);
340 | }
341 |
--------------------------------------------------------------------------------
/HorizontalGridView/src/com/opensource/widget/OnChildSelectedListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project.
3 | *
4 | * yinglovezhuzhu@gmail.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package com.opensource.widget;
19 |
20 | import android.view.View;
21 | import android.view.ViewGroup;
22 |
23 | /**
24 | * Interface definition for a callback to be invoked when a child of this
25 | * viewgroup has been selected.
26 | */
27 | public interface OnChildSelectedListener {
28 | /**
29 | * Callback method to be invoked when a child of this viewgroup has been
30 | * selected.
31 | *
32 | * This method may be called during layout, so implementations of this
33 | * interface need to be careful not to ... (todo).
34 | *
35 | * @param parent The ViewGroup where the selection happened.
36 | * @param view The view within the ViewGroup that is selected, or null if no
37 | * view is selected.
38 | * @param position The position of the view in the adapter, or NO_POSITION
39 | * if no view is selected.
40 | * @param id The id of the child that is selected, or NO_ID if no view is
41 | * selected.
42 | */
43 | void onChildSelected(ViewGroup parent, View view, int position, long id);
44 | }
45 |
--------------------------------------------------------------------------------
/HorizontalGridView/src/com/opensource/widget/StaggeredGrid.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project.
3 | *
4 | * yinglovezhuzhu@gmail.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package com.opensource.widget;
19 |
20 | import android.support.v4.util.CircularArray;
21 |
22 | import java.io.PrintWriter;
23 | import java.util.ArrayList;
24 | import java.util.List;
25 |
26 | /**
27 | * A dynamic data structure that maintains staggered grid position information
28 | * for each individual child. The algorithm ensures that each row will be kept
29 | * as balanced as possible when prepending and appending a child.
30 | *
31 | *
32 | * You may keep view {@link com.opensource.camcorder.widget.StaggeredGrid.Location} inside StaggeredGrid as much
33 | * as possible since prepending and appending views is not symmetric: layout
34 | * going from 0 to N will likely produce a different result than layout going
35 | * from N to 0 for the staggered cases. If a user scrolls from 0 to N then
36 | * scrolls back to 0 and we don't keep history location information, edges of
37 | * the very beginning of rows will not be aligned. It is recommended to keep a
38 | * list of tens of thousands of {@link com.opensource.camcorder.widget.StaggeredGrid.Location}s which will be
39 | * big enough to remember a typical user's scroll history. There are situations
40 | * where StaggeredGrid falls back to the simple case where we do not need save a
41 | * huge list of locations inside StaggeredGrid:
42 | *
43 | * - Only one row (e.g., a single row listview)
44 | * - Each item has the same length (not staggered at all)
45 | *
46 | *
47 | *
48 | * This class is abstract and can be replaced with different implementations.
49 | */
50 | abstract class StaggeredGrid {
51 |
52 | /**
53 | * TODO: document this
54 | */
55 | public static interface Provider {
56 | /**
57 | * Return how many items are in the adapter.
58 | */
59 | public abstract int getCount();
60 |
61 | /**
62 | * Create the object at a given row.
63 | */
64 | public abstract void createItem(int index, int row, boolean append);
65 | }
66 |
67 | /**
68 | * Location of an item in the grid. For now it only saves row index but
69 | * more information may be added in the future.
70 | */
71 | public final static class Location {
72 | /**
73 | * The index of the row for this Location.
74 | */
75 | public final int row;
76 |
77 | /**
78 | * Create a Location with the given row index.
79 | */
80 | public Location(int row) {
81 | this.row = row;
82 | }
83 | }
84 |
85 | /**
86 | * TODO: document this
87 | */
88 | public final static class Row {
89 | /**
90 | * first view start location
91 | */
92 | public int low;
93 | /**
94 | * last view end location
95 | */
96 | public int high;
97 | }
98 |
99 | protected Provider mProvider;
100 | protected int mNumRows = 1; // mRows.length
101 | protected Row[] mRows;
102 | protected CircularArray mLocations = new CircularArray(64);
103 | private ArrayList[] mTmpItemPositionsInRows;
104 |
105 | /**
106 | * A constant representing a default starting index, indicating that the
107 | * developer did not provide a start index.
108 | */
109 | public static final int START_DEFAULT = -1;
110 |
111 | // the first index that grid will layout
112 | protected int mStartIndex = START_DEFAULT;
113 | // the row to layout the first index
114 | protected int mStartRow = START_DEFAULT;
115 |
116 | protected int mFirstIndex = -1;
117 |
118 | /**
119 | * Sets the {@link com.opensource.camcorder.widget.StaggeredGrid.Provider} for this staggered grid.
120 | *
121 | * @param provider The provider for this staggered grid.
122 | */
123 | public void setProvider(Provider provider) {
124 | mProvider = provider;
125 | }
126 |
127 | /**
128 | * Sets the array of {@link com.opensource.camcorder.widget.StaggeredGrid.Row}s to fill into. For views that represent a
129 | * horizontal list, this will be the rows of the view. For views that
130 | * represent a vertical list, this will be the columns.
131 | *
132 | * @param row The array of {@link com.opensource.camcorder.widget.StaggeredGrid.Row}s to be filled.
133 | */
134 | public final void setRows(Row[] row) {
135 | if (row == null || row.length == 0) {
136 | throw new IllegalArgumentException();
137 | }
138 | mNumRows = row.length;
139 | mRows = row;
140 | mTmpItemPositionsInRows = new ArrayList[mNumRows];
141 | for (int i = 0; i < mNumRows; i++) {
142 | mTmpItemPositionsInRows[i] = new ArrayList(32);
143 | }
144 | }
145 |
146 | /**
147 | * Returns the number of rows in the staggered grid.
148 | */
149 | public final int getNumRows() {
150 | return mNumRows;
151 | }
152 |
153 | /**
154 | * Set the first item index and the row index to load when there are no
155 | * items.
156 | *
157 | * @param startIndex the index of the first item
158 | * @param startRow the index of the row
159 | */
160 | public final void setStart(int startIndex, int startRow) {
161 | mStartIndex = startIndex;
162 | mStartRow = startRow;
163 | }
164 |
165 | /**
166 | * Returns the first index in the staggered grid.
167 | */
168 | public final int getFirstIndex() {
169 | return mFirstIndex;
170 | }
171 |
172 | /**
173 | * Returns the last index in the staggered grid.
174 | */
175 | public final int getLastIndex() {
176 | return mFirstIndex + mLocations.size() - 1;
177 | }
178 |
179 | /**
180 | * Returns the size of the saved {@link com.opensource.camcorder.widget.StaggeredGrid.Location}s.
181 | */
182 | public final int getSize() {
183 | return mLocations.size();
184 | }
185 |
186 | /**
187 | * Returns the {@link com.opensource.camcorder.widget.StaggeredGrid.Location} at the given index.
188 | */
189 | public final Location getLocation(int index) {
190 | if (mLocations.size() == 0) {
191 | return null;
192 | }
193 | return mLocations.get(index - mFirstIndex);
194 | }
195 |
196 | /**
197 | * Removes the first element.
198 | */
199 | public final void removeFirst() {
200 | mFirstIndex++;
201 | mLocations.popFirst();
202 | }
203 |
204 | /**
205 | * Removes the last element.
206 | */
207 | public final void removeLast() {
208 | mLocations.popLast();
209 | }
210 |
211 | public final void debugPrint(PrintWriter pw) {
212 | for (int i = 0, size = mLocations.size(); i < size; i++) {
213 | Location loc = mLocations.get(i);
214 | pw.print("<" + (mFirstIndex + i) + "," + loc.row + ">");
215 | pw.print(" ");
216 | pw.println();
217 | }
218 | }
219 |
220 | protected final int getMaxHighRowIndex() {
221 | int maxHighRowIndex = 0;
222 | for (int i = 1; i < mNumRows; i++) {
223 | if (mRows[i].high > mRows[maxHighRowIndex].high) {
224 | maxHighRowIndex = i;
225 | }
226 | }
227 | return maxHighRowIndex;
228 | }
229 |
230 | protected final int getMinHighRowIndex() {
231 | int minHighRowIndex = 0;
232 | for (int i = 1; i < mNumRows; i++) {
233 | if (mRows[i].high < mRows[minHighRowIndex].high) {
234 | minHighRowIndex = i;
235 | }
236 | }
237 | return minHighRowIndex;
238 | }
239 |
240 | protected final Location appendItemToRow(int itemIndex, int rowIndex) {
241 | Location loc = new Location(rowIndex);
242 | if (mLocations.size() == 0) {
243 | mFirstIndex = itemIndex;
244 | }
245 | mLocations.addLast(loc);
246 | mProvider.createItem(itemIndex, rowIndex, true);
247 | return loc;
248 | }
249 |
250 | /**
251 | * Append items until the high edge reaches upTo.
252 | */
253 | public abstract void appendItems(int upTo);
254 |
255 | protected final int getMaxLowRowIndex() {
256 | int maxLowRowIndex = 0;
257 | for (int i = 1; i < mNumRows; i++) {
258 | if (mRows[i].low > mRows[maxLowRowIndex].low) {
259 | maxLowRowIndex = i;
260 | }
261 | }
262 | return maxLowRowIndex;
263 | }
264 |
265 | protected final int getMinLowRowIndex() {
266 | int minLowRowIndex = 0;
267 | for (int i = 1; i < mNumRows; i++) {
268 | if (mRows[i].low < mRows[minLowRowIndex].low) {
269 | minLowRowIndex = i;
270 | }
271 | }
272 | return minLowRowIndex;
273 | }
274 |
275 | protected final Location prependItemToRow(int itemIndex, int rowIndex) {
276 | Location loc = new Location(rowIndex);
277 | mFirstIndex = itemIndex;
278 | mLocations.addFirst(loc);
279 | mProvider.createItem(itemIndex, rowIndex, false);
280 | return loc;
281 | }
282 |
283 | /**
284 | * Return array of Lists for all rows, each List contains item positions
285 | * on that row between startPos(included) and endPositions(included).
286 | * Returned value is read only, do not change it.
287 | */
288 | public final List[] getItemPositionsInRows(int startPos, int endPos) {
289 | for (int i = 0; i < mNumRows; i++) {
290 | mTmpItemPositionsInRows[i].clear();
291 | }
292 | if (startPos >= 0) {
293 | for (int i = startPos; i <= endPos; i++) {
294 | mTmpItemPositionsInRows[getLocation(i).row].add(i);
295 | }
296 | }
297 | return mTmpItemPositionsInRows;
298 | }
299 |
300 | /**
301 | * Prepend items until the low edge reaches downTo.
302 | */
303 | public abstract void prependItems(int downTo);
304 |
305 | /**
306 | * Strip items, keep a contiguous subset of items; the subset should include
307 | * at least one item on every row that currently has at least one item.
308 | *
309 | *
310 | * TODO: document this better
311 | */
312 | public abstract void stripDownTo(int itemIndex);
313 | }
314 |
--------------------------------------------------------------------------------
/HorizontalGridView/src/com/opensource/widget/StaggeredGridDefault.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project.
3 | *
4 | * yinglovezhuzhu@gmail.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 | package com.opensource.widget;
19 |
20 | /**
21 | * A default implementation of {@link StaggeredGrid}.
22 | *
23 | * This implementation tries to fill items in consecutive row order. The next
24 | * item is always in same row or in the next row.
25 | */
26 | final class StaggeredGridDefault extends StaggeredGrid {
27 |
28 | @Override
29 | public void appendItems(int upTo) {
30 | int count = mProvider.getCount();
31 | int itemIndex;
32 | int rowIndex;
33 | if (mLocations.size() > 0) {
34 | itemIndex = getLastIndex() + 1;
35 | rowIndex = (mLocations.getLast().row + 1) % mNumRows;
36 | } else {
37 | itemIndex = mStartIndex != START_DEFAULT ? mStartIndex : 0;
38 | rowIndex = mStartRow != START_DEFAULT ? mStartRow : itemIndex % mNumRows;
39 | }
40 |
41 | top_loop:
42 | while (true) {
43 | // find highest row (.high is biggest)
44 | int maxHighRowIndex = mLocations.size() > 0 ? getMaxHighRowIndex() : -1;
45 | int maxHigh = maxHighRowIndex != -1 ? mRows[maxHighRowIndex].high : Integer.MIN_VALUE;
46 | // fill from current row till last row so that each row will grow longer than
47 | // the previous highest row.
48 | for (; rowIndex < mNumRows; rowIndex++) {
49 | // fill one item to a row
50 | if (itemIndex == count) {
51 | break top_loop;
52 | }
53 | appendItemToRow(itemIndex++, rowIndex);
54 | // fill more item to the row to make sure this row is longer than
55 | // the previous highest row.
56 | if (maxHighRowIndex == -1) {
57 | maxHighRowIndex = getMaxHighRowIndex();
58 | maxHigh = mRows[maxHighRowIndex].high;
59 | } else if (rowIndex != maxHighRowIndex) {
60 | while (mRows[rowIndex].high < maxHigh) {
61 | if (itemIndex == count) {
62 | break top_loop;
63 | }
64 | appendItemToRow(itemIndex++, rowIndex);
65 | }
66 | }
67 | }
68 | if (mRows[getMinHighRowIndex()].high >= upTo) {
69 | break;
70 | }
71 | // start fill from row 0 again
72 | rowIndex = 0;
73 | }
74 | }
75 |
76 | @Override
77 | public void prependItems(int downTo) {
78 | if (mProvider.getCount() <= 0) return;
79 | int itemIndex;
80 | int rowIndex;
81 | if (mLocations.size() > 0) {
82 | itemIndex = getFirstIndex() - 1;
83 | rowIndex = mLocations.getFirst().row;
84 | if (rowIndex == 0) {
85 | rowIndex = mNumRows - 1;
86 | } else {
87 | rowIndex--;
88 | }
89 | } else {
90 | itemIndex = mStartIndex != START_DEFAULT ? mStartIndex : 0;
91 | rowIndex = mStartRow != START_DEFAULT ? mStartRow : itemIndex % mNumRows;
92 | }
93 |
94 | top_loop:
95 | while (true) {
96 | int minLowRowIndex = mLocations.size() > 0 ? getMinLowRowIndex() : -1;
97 | int minLow = minLowRowIndex != -1 ? mRows[minLowRowIndex].low : Integer.MAX_VALUE;
98 | for (; rowIndex >=0 ; rowIndex--) {
99 | if (itemIndex < 0) {
100 | break top_loop;
101 | }
102 | prependItemToRow(itemIndex--, rowIndex);
103 | if (minLowRowIndex == -1) {
104 | minLowRowIndex = getMinLowRowIndex();
105 | minLow = mRows[minLowRowIndex].low;
106 | } else if (rowIndex != minLowRowIndex) {
107 | while (mRows[rowIndex].low > minLow) {
108 | if (itemIndex < 0) {
109 | break top_loop;
110 | }
111 | prependItemToRow(itemIndex--, rowIndex);
112 | }
113 | }
114 | }
115 | if (mRows[getMaxLowRowIndex()].low <= downTo) {
116 | break;
117 | }
118 | rowIndex = mNumRows - 1;
119 | }
120 | }
121 |
122 | @Override
123 | public final void stripDownTo(int itemIndex) {
124 | // because we layout the items in the order that next item is either same row
125 | // or next row, so we can easily find the row range by searching items forward and
126 | // backward until we see the row is 0 or mNumRow - 1
127 | Location loc = getLocation(itemIndex);
128 | if (loc == null) {
129 | return;
130 | }
131 | int firstIndex = getFirstIndex();
132 | int lastIndex = getLastIndex();
133 | int row = loc.row;
134 |
135 | int endIndex = itemIndex;
136 | int endRow = row;
137 | while (endRow < mNumRows - 1 && endIndex < lastIndex) {
138 | endIndex++;
139 | endRow = getLocation(endIndex).row;
140 | }
141 |
142 | int startIndex = itemIndex;
143 | int startRow = row;
144 | while (startRow > 0 && startIndex > firstIndex) {
145 | startIndex--;
146 | startRow = getLocation(startIndex).row;
147 | }
148 | // trim information
149 | for (int i = firstIndex; i < startIndex; i++) {
150 | removeFirst();
151 | }
152 | for (int i = endIndex; i < lastIndex; i++) {
153 | removeLast();
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/HorizontalGridView/src/com/opensource/widget/WindowAlignment.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project.
3 | *
4 | * yinglovezhuzhu@gmail.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | package com.opensource.widget;
20 |
21 | import static com.opensource.widget.RecyclerView.HORIZONTAL;
22 | import static com.opensource.widget.BaseGridView.WINDOW_ALIGN_BOTH_EDGE;
23 | import static com.opensource.widget.BaseGridView.WINDOW_ALIGN_HIGH_EDGE;
24 | import static com.opensource.widget.BaseGridView.WINDOW_ALIGN_LOW_EDGE;
25 | import static com.opensource.widget.BaseGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED;
26 |
27 | /**
28 | * Maintains Window Alignment information of two axis.
29 | */
30 | class WindowAlignment {
31 |
32 | /**
33 | * Maintains alignment information in one direction.
34 | */
35 | public static class Axis {
36 | /**
37 | * mScrollCenter is used to calculate dynamic transformation based on how far a view
38 | * is from the mScrollCenter. For example, the views with center close to mScrollCenter
39 | * will be scaled up.
40 | */
41 | private float mScrollCenter;
42 | /**
43 | * Right or bottom edge of last child.
44 | */
45 | private int mMaxEdge;
46 | /**
47 | * Left or top edge of first child, typically should be zero.
48 | */
49 | private int mMinEdge;
50 | /**
51 | * Max Scroll value
52 | */
53 | private int mMaxScroll;
54 | /**
55 | * Min Scroll value
56 | */
57 | private int mMinScroll;
58 |
59 | private int mWindowAlignment = WINDOW_ALIGN_BOTH_EDGE;
60 |
61 | private int mWindowAlignmentOffset = 0;
62 |
63 | private float mWindowAlignmentOffsetPercent = 50f;
64 |
65 | private int mSize;
66 |
67 | private int mPaddingLow;
68 |
69 | private int mPaddingHigh;
70 |
71 | private String mName; // for debugging
72 |
73 | public Axis(String name) {
74 | reset();
75 | mName = name;
76 | }
77 |
78 | final public int getWindowAlignment() {
79 | return mWindowAlignment;
80 | }
81 |
82 | final public void setWindowAlignment(int windowAlignment) {
83 | mWindowAlignment = windowAlignment;
84 | }
85 |
86 | final public int getWindowAlignmentOffset() {
87 | return mWindowAlignmentOffset;
88 | }
89 |
90 | final public void setWindowAlignmentOffset(int offset) {
91 | mWindowAlignmentOffset = offset;
92 | }
93 |
94 | final public void setWindowAlignmentOffsetPercent(float percent) {
95 | if ((percent < 0 || percent > 100)
96 | && percent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) {
97 | throw new IllegalArgumentException();
98 | }
99 | mWindowAlignmentOffsetPercent = percent;
100 | }
101 |
102 | final public float getWindowAlignmentOffsetPercent() {
103 | return mWindowAlignmentOffsetPercent;
104 | }
105 |
106 | final public int getScrollCenter() {
107 | return (int) mScrollCenter;
108 | }
109 |
110 | /** set minEdge, Integer.MIN_VALUE means unknown*/
111 | final public void setMinEdge(int minEdge) {
112 | mMinEdge = minEdge;
113 | }
114 |
115 | final public int getMinEdge() {
116 | return mMinEdge;
117 | }
118 |
119 | /** set minScroll, Integer.MIN_VALUE means unknown*/
120 | final public void setMinScroll(int minScroll) {
121 | mMinScroll = minScroll;
122 | }
123 |
124 | final public int getMinScroll() {
125 | return mMinScroll;
126 | }
127 |
128 | final public void invalidateScrollMin() {
129 | mMinEdge = Integer.MIN_VALUE;
130 | mMinScroll = Integer.MIN_VALUE;
131 | }
132 |
133 | /** update max edge, Integer.MAX_VALUE means unknown*/
134 | final public void setMaxEdge(int maxEdge) {
135 | mMaxEdge = maxEdge;
136 | }
137 |
138 | final public int getMaxEdge() {
139 | return mMaxEdge;
140 | }
141 |
142 | /** update max scroll, Integer.MAX_VALUE means unknown*/
143 | final public void setMaxScroll(int maxScroll) {
144 | mMaxScroll = maxScroll;
145 | }
146 |
147 | final public int getMaxScroll() {
148 | return mMaxScroll;
149 | }
150 |
151 | final public void invalidateScrollMax() {
152 | mMaxEdge = Integer.MAX_VALUE;
153 | mMaxScroll = Integer.MAX_VALUE;
154 | }
155 |
156 | final public float updateScrollCenter(float scrollTarget) {
157 | mScrollCenter = scrollTarget;
158 | return scrollTarget;
159 | }
160 |
161 | private void reset() {
162 | mScrollCenter = Integer.MIN_VALUE;
163 | mMinEdge = Integer.MIN_VALUE;
164 | mMaxEdge = Integer.MAX_VALUE;
165 | }
166 |
167 | final public boolean isMinUnknown() {
168 | return mMinEdge == Integer.MIN_VALUE;
169 | }
170 |
171 | final public boolean isMaxUnknown() {
172 | return mMaxEdge == Integer.MAX_VALUE;
173 | }
174 |
175 | final public void setSize(int size) {
176 | mSize = size;
177 | }
178 |
179 | final public int getSize() {
180 | return mSize;
181 | }
182 |
183 | final public void setPadding(int paddingLow, int paddingHigh) {
184 | mPaddingLow = paddingLow;
185 | mPaddingHigh = paddingHigh;
186 | }
187 |
188 | final public int getPaddingLow() {
189 | return mPaddingLow;
190 | }
191 |
192 | final public int getPaddingHigh() {
193 | return mPaddingHigh;
194 | }
195 |
196 | final public int getClientSize() {
197 | return mSize - mPaddingLow - mPaddingHigh;
198 | }
199 |
200 | final public int getSystemScrollPos() {
201 | return getSystemScrollPos((int) mScrollCenter);
202 | }
203 |
204 | final public int getSystemScrollPos(int scrollCenter) {
205 | int middlePosition;
206 | if (mWindowAlignmentOffset >= 0) {
207 | middlePosition = mWindowAlignmentOffset - mPaddingLow;
208 | } else {
209 | middlePosition = mSize + mWindowAlignmentOffset - mPaddingLow;
210 | }
211 | if (mWindowAlignmentOffsetPercent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) {
212 | middlePosition += (int) (mSize * mWindowAlignmentOffsetPercent / 100);
213 | }
214 | int clientSize = getClientSize();
215 | int afterMiddlePosition = clientSize - middlePosition;
216 | boolean isMinUnknown = isMinUnknown();
217 | boolean isMaxUnknown = isMaxUnknown();
218 | if (!isMinUnknown && !isMaxUnknown &&
219 | (mWindowAlignment & WINDOW_ALIGN_BOTH_EDGE) == WINDOW_ALIGN_BOTH_EDGE) {
220 | if (mMaxEdge - mMinEdge <= clientSize) {
221 | // total children size is less than view port and we want to align
222 | // both edge: align first child to left edge of view port
223 | return mMinEdge - mPaddingLow;
224 | }
225 | }
226 | if (!isMinUnknown) {
227 | if ((mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0 &&
228 | scrollCenter - mMinEdge <= middlePosition) {
229 | // scroll center is within half of view port size: align the left edge
230 | // of first child to the left edge of view port
231 | return mMinEdge - mPaddingLow;
232 | }
233 | }
234 | if (!isMaxUnknown) {
235 | if ((mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0 &&
236 | mMaxEdge - scrollCenter <= afterMiddlePosition) {
237 | // scroll center is very close to the right edge of view port : align the
238 | // right edge of last children (plus expanded size) to view port's right
239 | return mMaxEdge -mPaddingLow - (clientSize);
240 | }
241 | }
242 | // else put scroll center in middle of view port
243 | return scrollCenter - middlePosition - mPaddingLow;
244 | }
245 |
246 | @Override
247 | public String toString() {
248 | return "center: " + mScrollCenter + " min:" + mMinEdge +
249 | " max:" + mMaxEdge;
250 | }
251 |
252 | }
253 |
254 | private int mOrientation = HORIZONTAL;
255 |
256 | final public Axis vertical = new Axis("vertical");
257 |
258 | final public Axis horizontal = new Axis("horizontal");
259 |
260 | private Axis mMainAxis = horizontal;
261 |
262 | private Axis mSecondAxis = vertical;
263 |
264 | final public Axis mainAxis() {
265 | return mMainAxis;
266 | }
267 |
268 | final public Axis secondAxis() {
269 | return mSecondAxis;
270 | }
271 |
272 | final public void setOrientation(int orientation) {
273 | mOrientation = orientation;
274 | if (mOrientation == HORIZONTAL) {
275 | mMainAxis = horizontal;
276 | mSecondAxis = vertical;
277 | } else {
278 | mMainAxis = vertical;
279 | mSecondAxis = horizontal;
280 | }
281 | }
282 |
283 | final public int getOrientation() {
284 | return mOrientation;
285 | }
286 |
287 | final public void reset() {
288 | mainAxis().reset();
289 | }
290 |
291 | @Override
292 | public String toString() {
293 | return new StringBuffer().append("horizontal=")
294 | .append(horizontal.toString())
295 | .append("vertical=")
296 | .append(vertical.toString())
297 | .toString();
298 | }
299 |
300 | }
301 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/.gitignore:
--------------------------------------------------------------------------------
1 | /gen/
2 | /bin/
3 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | HorizontalGridView_Demo
4 |
5 |
6 |
7 |
8 |
9 | com.android.ide.eclipse.adt.ResourceManagerBuilder
10 |
11 |
12 |
13 |
14 | com.android.ide.eclipse.adt.PreCompilerBuilder
15 |
16 |
17 |
18 |
19 | org.eclipse.jdt.core.javabuilder
20 |
21 |
22 |
23 |
24 | com.android.ide.eclipse.adt.ApkBuilder
25 |
26 |
27 |
28 |
29 |
30 | com.android.ide.eclipse.adt.AndroidNature
31 | org.eclipse.jdt.core.javanature
32 |
33 |
34 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/.settings/org.eclipse.jdt.core.prefs:
--------------------------------------------------------------------------------
1 | eclipse.preferences.version=1
2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
3 | org.eclipse.jdt.core.compiler.compliance=1.6
4 | org.eclipse.jdt.core.compiler.source=1.6
5 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yinglovezhuzhu/HorizontalGridView/5322c3c0cbabe194a9af5b63285859bfc2b758d0/HorizontalGridView_Demo/ic_launcher-web.png
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/proguard-project.txt:
--------------------------------------------------------------------------------
1 | # To enable ProGuard in your project, edit project.properties
2 | # to define the proguard.config property as described in that file.
3 | #
4 | # Add project specific ProGuard rules here.
5 | # By default, the flags in this file are appended to flags specified
6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt
7 | # You can edit the include path and order by changing the ProGuard
8 | # include property in project.properties.
9 | #
10 | # For more details, see
11 | # http://developer.android.com/guide/developing/tools/proguard.html
12 |
13 | # Add any project specific keep options here:
14 |
15 | # If your project uses WebView with JS, uncomment the following
16 | # and specify the fully qualified class name to the JavaScript interface
17 | # class:
18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
19 | # public *;
20 | #}
21 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 |
13 | # Project target.
14 | target=android-19
15 | android.library.reference.1=../HorizontalGridView
16 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yinglovezhuzhu/HorizontalGridView/5322c3c0cbabe194a9af5b63285859bfc2b758d0/HorizontalGridView_Demo/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yinglovezhuzhu/HorizontalGridView/5322c3c0cbabe194a9af5b63285859bfc2b758d0/HorizontalGridView_Demo/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yinglovezhuzhu/HorizontalGridView/5322c3c0cbabe194a9af5b63285859bfc2b758d0/HorizontalGridView_Demo/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yinglovezhuzhu/HorizontalGridView/5322c3c0cbabe194a9af5b63285859bfc2b758d0/HorizontalGridView_Demo/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
18 |
19 |
29 |
30 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/res/values-v11/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 | 16dp
22 | 16dp
23 |
24 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | HorizontalGridView_Demo
5 | Hello world!
6 |
7 |
8 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
14 |
15 |
16 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/HorizontalGridView_Demo/src/com/opensource/widget/horizontalgridview/MainActivity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project.
3 | *
4 | * yinglovezhuzhu@gmail.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | package com.opensource.widget.horizontalgridview;
20 |
21 | import android.app.Activity;
22 | import android.os.Bundle;
23 | import android.util.Log;
24 | import android.view.Menu;
25 | import android.view.MenuItem;
26 | import android.view.View;
27 | import android.view.ViewGroup;
28 | import android.widget.ImageView;
29 |
30 | import com.opensource.widget.HorizontalGridView;
31 | import com.opensource.widget.OnChildSelectedListener;
32 | import com.opensource.widget.RecyclerView;
33 |
34 |
35 | public class MainActivity extends Activity {
36 |
37 | private HorizontalGridView mHorizontalGridView;
38 | @Override
39 | protected void onCreate(Bundle savedInstanceState) {
40 | super.onCreate(savedInstanceState);
41 | setContentView(R.layout.activity_main);
42 |
43 | mHorizontalGridView = (HorizontalGridView) findViewById(R.id.hgv);
44 | mHorizontalGridView.setAdapter(new RecyclerView.Adapter() {
45 | @Override
46 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
47 | ImageView iv = new ImageView(MainActivity.this);
48 | iv.setScaleType(ImageView.ScaleType.CENTER_CROP);
49 | iv.setImageResource(R.drawable.ic_launcher);
50 | final RecyclerView.ViewHolder viewHolder = new ViewHolder(iv);
51 | //TODO 把Item的点击事件在这里做,调用View.setOnClickListener(View.OnClickListener),然后用viewHoder.getPosition()来获取点击的位置
52 | //以同样的方法可以实现OnLongClickListener等
53 | //注意:RecyclerView.ViewHolder不能直接添加监听,可以用RecyclerView.ViewHolder.itemView添加监听
54 | viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
55 | @Override
56 | public void onClick(View v) {
57 | mHorizontalGridView.setSelectedPosition(viewHolder.getPosition());
58 | }
59 | });
60 | return viewHolder;
61 | }
62 |
63 | @Override
64 | public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) {
65 | //TODO 把一些View的相关改变在这里实现
66 | }
67 |
68 | @Override
69 | public int getItemCount() {
70 | return 50;
71 | }
72 | });
73 | mHorizontalGridView.setOnChildSelectedListener(new OnChildSelectedListener() {
74 | @Override
75 | public void onChildSelected(ViewGroup parent, View view, int position, long id) {
76 | Log.i("MainActivity", "Item " + position + " was selected");
77 | }
78 | });
79 | }
80 |
81 |
82 | @Override
83 | public boolean onCreateOptionsMenu(Menu menu) {
84 | // Inflate the menu; this adds items to the action bar if it is present.
85 | return true;
86 | }
87 |
88 | @Override
89 | public boolean onOptionsItemSelected(MenuItem item) {
90 | // Handle action bar item clicks here. The action bar will
91 | // automatically handle clicks on the Home/Up button, so long
92 | // as you specify a parent activity in AndroidManifest.xml.
93 | return super.onOptionsItemSelected(item);
94 | }
95 |
96 | public static class ViewHolder extends RecyclerView.ViewHolder {
97 |
98 | public ViewHolder(View itemView) {
99 | super(itemView);
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | HorizontalGridView
2 | ==================
3 |
4 | 这是一个从suport-v17中提取的HorizotalGridView,是一个水平滚动的GridView
5 |
--------------------------------------------------------------------------------