├── .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 | *

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 | * 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 | *

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 | --------------------------------------------------------------------------------