├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── vilyever │ │ └── howtocustomlayoutmanager │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── vilyever │ │ │ └── howtocustomlayoutmanager │ │ │ ├── CustomLayoutManager.java │ │ │ ├── DataAdapter.java │ │ │ ├── DemoModel.java │ │ │ ├── DemoViewHolder.java │ │ │ ├── MainActivity.java │ │ │ ├── NumberPickerDialog.java │ │ │ └── ViewHolderCreator.java │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── content_main.xml │ │ └── demo_view_holder.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── vilyever │ └── howtocustomlayoutmanager │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── snapshots ├── device-2016-01-07-145618.png ├── device-2016-01-08-104053.png └── device-2016-01-08-145727.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Android Studio Navigation editor temp files 29 | .navigation/ 30 | 31 | # Android Studio captures folder 32 | captures/ 33 | 34 | # Android Studio 35 | *.iml 36 | .idea 37 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 38 | .gradle 39 | build/ 40 | 41 | # Eclipse project files 42 | .classpath 43 | .project 44 | 45 | #NDK 46 | obj/ 47 | 48 | # OSX files 49 | .DS_Store 50 | 51 | # Windows thumbnail db 52 | Thumbs.db 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HowToCustomLayoutManager 2 | 展示自定义RecyclerView的LayoutManager的过程,可以按顺序检出tag来查看每个阶段的代码 3 | 4 | ### 1.0.3 5 | 展示一个实现了重用和横纵向滑动的按块分配布局的CustomLayoutManager,支持smoothScroll 6 |

7 | 8 |

9 | 10 | ### 1.0.2 11 | 展示一个实现了重用和横纵向滑动的斜线布局的CustomLayoutManager 12 | 13 | ### 1.0.1 14 | 展示一个简单斜线布局的CustomLayoutManager,没有实现重用和滑动 15 |

16 | 17 |

18 | 19 | ### 1.0.0 20 | 展示一个使用[LinearLayoutManager](http://developer.android.com/reference/android/support/v7/widget/LinearLayoutManager.html)布局的示例 21 |

22 | 23 |

24 | 25 | ## License 26 | [Apache License Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 27 | 28 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Android Studio Navigation editor temp files 29 | .navigation/ 30 | 31 | # Android Studio captures folder 32 | captures/ 33 | 34 | # Android Studio 35 | *.iml 36 | .idea 37 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 38 | .gradle 39 | build/ 40 | 41 | # Eclipse project files 42 | .classpath 43 | .project 44 | 45 | #NDK 46 | obj/ 47 | 48 | # OSX files 49 | .DS_Store 50 | 51 | # Windows thumbnail db 52 | Thumbs.db 53 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | applicationId "com.vilyever.howtocustomlayoutmanager" 9 | minSdkVersion 16 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:23.1.1' 26 | compile 'com.android.support:design:23.1.1' 27 | } 28 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Development\Android\Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/vilyever/howtocustomlayoutmanager/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.vilyever.howtocustomlayoutmanager; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/vilyever/howtocustomlayoutmanager/CustomLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.vilyever.howtocustomlayoutmanager; 2 | 3 | import android.content.Context; 4 | import android.graphics.PointF; 5 | import android.graphics.Rect; 6 | import android.support.annotation.IntDef; 7 | import android.support.v7.widget.LinearSmoothScroller; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.util.AttributeSet; 10 | import android.util.SparseArray; 11 | import android.util.SparseBooleanArray; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.widget.LinearLayout; 15 | 16 | import java.lang.annotation.Retention; 17 | import java.lang.annotation.RetentionPolicy; 18 | 19 | /** 20 | * CustomLayoutManager 21 | * HowToCustomLayoutManager 22 | * Created by vilyever on 2016/1/7. 23 | * Feature: 24 | * 展示一个斜线布局的示例 25 | * 支持横向纵向滑动 26 | */ 27 | public class CustomLayoutManager extends RecyclerView.LayoutManager { 28 | /** Convenience Var to call this */ 29 | final CustomLayoutManager self = this; 30 | 31 | /** 32 | * init with orientation and itemLineCount 33 | * @param orientation {@link com.vilyever.howtocustomlayoutmanager.CustomLayoutManager.State#orientation} 34 | * @param itemLineCount {@link com.vilyever.howtocustomlayoutmanager.CustomLayoutManager.State#itemLineCount} 35 | */ 36 | public CustomLayoutManager(@Orientation int orientation, int itemLineCount) { 37 | setOrientation(orientation); 38 | setItemLineCount(itemLineCount); 39 | } 40 | 41 | /** @see android.support.v7.widget.RecyclerView.LayoutManager#onLayoutChildren(RecyclerView.Recycler, RecyclerView.State) */ 42 | @Override 43 | public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 44 | if (state.isPreLayout()) { // 跳过preLayout,preLayout主要用于支持动画,暂时先使用自带的简单的fading 45 | return; 46 | } 47 | self.detachAndScrapAttachedViews(recycler); // 分离所有的itemView 48 | 49 | self.getState().contentWidth = 0; 50 | self.getState().contentHeight = 0; 51 | 52 | if (getItemCount() == 0) { 53 | self.getState().scrolledX = 0; 54 | self.getState().scrolledY = 0; 55 | self.getState().totalSpreadCount = 0; 56 | return; 57 | } 58 | 59 | self.getState().totalSpreadCount = 1; 60 | 61 | int offsetX = 0; // 当前X偏移,下一个item的left为此值 62 | int offsetY = 0; // 当前Y偏移,下一个item的top为此值 63 | int occupiedLineBlock = 0; // 当前line已被占用的块数 64 | 65 | /** 66 | * {@link #orientation} 为 {@link #HORIZONTAL} 时,当前line的最大宽度 67 | * {@link #orientation} 为 {@link #VERTICAL} 时,当前line的最大高度 68 | */ 69 | int currentLineSpreadLength = 0; 70 | 71 | /** 72 | * {@link #orientation} 为 {@link #HORIZONTAL} 时,当前line的总高度 73 | * {@link #orientation} 为 {@link #VERTICAL} 时,当前line的总宽度 74 | */ 75 | int currentLineLength = 0; 76 | 77 | /** 78 | * {@link #orientation} 为 {@link #HORIZONTAL} 时,所有line的最大高度 79 | * {@link #orientation} 为 {@link #VERTICAL} 时,所有line的最大宽度 80 | */ 81 | int maxLineLength = 0; 82 | 83 | /** 84 | * 计算所有item的frame,以及总宽高 85 | */ 86 | for (int i = 0; i < getItemCount(); i++) { 87 | View scrap = recycler.getViewForPosition(i); // 根据position获取一个碎片view,可以从回收的view中获取,也可能新构造一个 88 | 89 | self.addView(scrap); 90 | self.measureChildWithMargins(scrap, 0, 0); // 计算此碎片view包含边距的尺寸 91 | 92 | int width = self.getDecoratedMeasuredWidth(scrap); // 获取此碎片view包含边距和装饰的宽度width 93 | int height = self.getDecoratedMeasuredHeight(scrap); // 获取此碎片view包含边距和装饰的高度height 94 | 95 | LayoutParams layoutParams = (LayoutParams) scrap.getLayoutParams(); 96 | occupiedLineBlock += Math.max(layoutParams.occupationLineBlocks, 1); // 记录当前line已占用的块数,每个item至少占1块 97 | /** 占用的块数若超过了{@link com.vilyever.howtocustomlayoutmanager.CustomLayoutManager.State#itemLineCount},切换到下一line */ 98 | if (occupiedLineBlock > self.getState().itemLineCount) { 99 | if (self.getState().orientation == HORIZONTAL) { 100 | offsetX += currentLineSpreadLength; // 横向偏移当前line的最大宽度 101 | offsetY = 0; // 纵向重置到0 102 | self.getState().contentWidth += currentLineSpreadLength; // contentWidth增加当前line的最大宽度 103 | } 104 | else { 105 | offsetX = 0; // 横向重置到0 106 | offsetY += currentLineSpreadLength; // 纵向偏移当前line的最大高度 107 | self.getState().contentHeight += currentLineSpreadLength; // contentHeight增加当前line的最大高度 108 | } 109 | 110 | occupiedLineBlock = layoutParams.occupationLineBlocks; // 切换到新line布置item 111 | currentLineSpreadLength = 0; // 新line还没有item,不占空间 112 | maxLineLength = Math.max(maxLineLength, currentLineLength); // 记录之前所有line的最大长度 113 | currentLineLength = 0; // 重置 114 | self.getState().totalSpreadCount++; 115 | } 116 | 117 | Rect frame = self.getState().itemsFrames.get(i); // 若先前生成过Rect,重复使用 118 | if (frame == null) { 119 | frame = new Rect(); 120 | } 121 | 122 | frame.set(offsetX, offsetY, offsetX + width, offsetY + height); 123 | self.getState().itemsFrames.put(i, frame); // 记录每个item的frame 124 | self.getState().itemsAttached.put(i, false); // 因为先前已经回收了所有item,此处将item显示标识置否 125 | 126 | if (self.getState().orientation == HORIZONTAL) { 127 | offsetY += height; // 纵向偏移,横向不变 128 | currentLineSpreadLength = Math.max(currentLineSpreadLength, width); // 记录当前line最大宽度 129 | currentLineLength += height; // 记录当前line总高度 130 | } 131 | else { 132 | offsetX += width; // 横向偏移,纵向不变 133 | currentLineSpreadLength = Math.max(currentLineSpreadLength, height); // 记录当前line最大高度 134 | currentLineLength += width; // 记录当前line总宽度 135 | } 136 | 137 | self.detachAndScrapView(scrap, recycler); // 回收本次计算的碎片view 138 | } 139 | 140 | if (self.getState().orientation == HORIZONTAL) { 141 | self.getState().contentWidth += currentLineSpreadLength; // contentWidth增加最后line的最大宽度 142 | self.getState().contentHeight = maxLineLength; // contentHeight设为所有line的最大高度 143 | } 144 | else { 145 | self.getState().contentWidth = maxLineLength; // contentWidth设为所有line的最大宽度 146 | self.getState().contentHeight += currentLineSpreadLength; // contentWidth增加最后line的最大高度 147 | } 148 | 149 | self.getState().contentWidth = Math.max(self.getState().contentWidth, self.getHorizontalSpace()); // 内容宽度最小为RecyclerView容器宽度 150 | self.getState().contentHeight = Math.max(self.getState().contentHeight, self.getVerticalSpace()); // 内容高度最小为RecyclerView容器高度 151 | 152 | // 依照新内容宽高调整记录的滑动距离,防止滑动偏移过大 153 | self.fixScrollOffset(); 154 | 155 | self.layoutItems(recycler, state); // 放置当前scroll offset处要展示的item 156 | } 157 | 158 | /** @see RecyclerView.LayoutManager#canScrollHorizontally() */ 159 | @Override 160 | public boolean canScrollHorizontally() { 161 | return self.getState().canScrollHorizontal; 162 | } 163 | 164 | /** @see android.support.v7.widget.RecyclerView.LayoutManager#scrollHorizontallyBy(int, RecyclerView.Recycler, RecyclerView.State) */ 165 | @Override 166 | public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { 167 | int willScroll = dx; 168 | /** 169 | * 限制滑动距离的最小值和最大值 170 | */ 171 | if (self.getState().scrolledX + dx < 0) { 172 | willScroll = -self.getState().scrolledX; 173 | } 174 | else if (self.getState().scrolledX + dx > self.getState().contentWidth - self.getHorizontalSpace()) { 175 | willScroll = self.getState().contentWidth - self.getHorizontalSpace() - self.getState().scrolledX; 176 | } 177 | 178 | // 如果将要滑动的距离为0,返回-dx以显示边缘光晕 179 | if (willScroll == 0) { 180 | return -dx; 181 | } 182 | 183 | self.getState().scrolledX += willScroll; 184 | 185 | // 平移容器内的item 186 | self.offsetChildrenHorizontal(-willScroll); 187 | // 移除屏幕外的item,添加当前可显示的新item 188 | self.layoutItems(recycler, state); 189 | 190 | return willScroll; 191 | } 192 | 193 | /** @see RecyclerView.LayoutManager#canScrollVertically() */ 194 | @Override 195 | public boolean canScrollVertically() { 196 | return self.getState().canScrollVertical; 197 | } 198 | 199 | /** @see android.support.v7.widget.RecyclerView.LayoutManager#scrollVerticallyBy(int, RecyclerView.Recycler, RecyclerView.State) */ 200 | @Override 201 | public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { 202 | int willScroll = dy; 203 | 204 | /** 205 | * 限制滑动距离的最小值和最大值 206 | */ 207 | if (self.getState().scrolledY + dy < 0) { 208 | willScroll = -self.getState().scrolledY; 209 | } 210 | else if (self.getState().scrolledY + dy > self.getState().contentHeight - self.getVerticalSpace()) { 211 | willScroll = self.getState().contentHeight - self.getVerticalSpace() - self.getState().scrolledY; 212 | } 213 | 214 | // 如果将要滑动的距离为0,返回-dy以显示边缘光晕 215 | if (willScroll == 0) { 216 | return -dy; 217 | } 218 | 219 | self.getState().scrolledY += willScroll; 220 | 221 | // 平移容器内的item 222 | self.offsetChildrenVertical(-willScroll); 223 | // 移除屏幕外的item,添加当前可显示的新item 224 | self.layoutItems(recycler, state); 225 | 226 | return willScroll; 227 | } 228 | 229 | @Override 230 | public void scrollToPosition(int position) { 231 | position = Math.max(position, 0); 232 | position = Math.min(position, self.getItemCount()); 233 | 234 | self.getState().scrolledX = self.getState().itemsFrames.get(position).left; 235 | self.getState().scrolledY = self.getState().itemsFrames.get(position).top; 236 | self.fixScrollOffset(); 237 | 238 | self.requestLayout(); 239 | } 240 | 241 | @Override 242 | public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { 243 | position = Math.max(position, 0); 244 | position = Math.min(position, self.getItemCount()); 245 | 246 | /* 247 | * LinearSmoothScroller's default behavior is to scroll the contents until 248 | * the child is fully visible. It will snap to the top-left or bottom-right 249 | * of the parent depending on whether the direction of travel was positive 250 | * or negative. 251 | */ 252 | LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) { 253 | /* 254 | * LinearSmoothScroller, at a minimum, just need to know the vector 255 | * (x/y distance) to travel in order to get from the current positioning 256 | * to the target. 257 | */ 258 | @Override 259 | public PointF computeScrollVectorForPosition(int targetPosition) { 260 | int oldScrollX = self.getState().scrolledX; 261 | int oldScrollY = self.getState().scrolledY; 262 | 263 | self.getState().scrolledX = self.getState().itemsFrames.get(targetPosition).left; 264 | self.getState().scrolledY = self.getState().itemsFrames.get(targetPosition).top; 265 | self.fixScrollOffset(); 266 | 267 | int newScrollX = self.getState().scrolledX; 268 | int newScrollY = self.getState().scrolledY; 269 | 270 | self.getState().scrolledX = oldScrollX; 271 | self.getState().scrolledY = oldScrollY; 272 | 273 | return new PointF(newScrollX - oldScrollX, newScrollY - oldScrollY); 274 | } 275 | }; 276 | scroller.setTargetPosition(position); 277 | self.startSmoothScroll(scroller); 278 | } 279 | 280 | /** @see RecyclerView.LayoutManager#generateDefaultLayoutParams() */ 281 | @Override 282 | public LayoutParams generateDefaultLayoutParams() { 283 | return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 284 | } 285 | /** @see RecyclerView.LayoutManager#generateLayoutParams(ViewGroup.LayoutParams) */ 286 | @Override 287 | public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 288 | if (lp instanceof ViewGroup.MarginLayoutParams) { 289 | return new LayoutParams((ViewGroup.MarginLayoutParams) lp); 290 | } else { 291 | return new LayoutParams(lp); 292 | } 293 | } 294 | /** @see RecyclerView.LayoutManager#generateLayoutParams(Context, AttributeSet) */ 295 | @Override 296 | public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 297 | return new LayoutParams(c, attrs); 298 | } 299 | /** @see RecyclerView.LayoutManager#checkLayoutParams(RecyclerView.LayoutParams) */ 300 | @Override 301 | public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 302 | return lp instanceof LayoutParams; 303 | } 304 | 305 | public static class LayoutParams extends RecyclerView.LayoutParams { 306 | 307 | /** 308 | * 占用块数 309 | * 不同方向布局时,占用行数或列数的值 310 | * {@link State#itemLineCount} 311 | * default is 1, must > 0 312 | */ 313 | public int occupationLineBlocks = 1; 314 | 315 | public LayoutParams(Context c, AttributeSet attrs) { 316 | super(c, attrs); 317 | } 318 | public LayoutParams(int width, int height) { 319 | super(width, height); 320 | } 321 | public LayoutParams(ViewGroup.MarginLayoutParams source) { 322 | super(source); 323 | } 324 | public LayoutParams(ViewGroup.LayoutParams source) { 325 | super(source); 326 | } 327 | public LayoutParams(RecyclerView.LayoutParams source) { 328 | super(source); 329 | } 330 | } 331 | 332 | /** 333 | * 摆放当前状态下要展示的item 334 | * @param recycler Recycler to use for fetching potentially cached views for a 335 | * position 336 | * @param state Transient state of RecyclerView 337 | */ 338 | private void layoutItems(RecyclerView.Recycler recycler, RecyclerView.State state) { 339 | if (state.isPreLayout()) { // 跳过preLayout,preLayout主要用于支持动画 340 | return; 341 | } 342 | 343 | // 当前scroll offset状态下的显示区域 344 | Rect displayFrame = new Rect(self.getState().scrolledX, self.getState().scrolledY, self.getState().scrolledX + self.getHorizontalSpace(), self.getState().scrolledY + self.getVerticalSpace()); 345 | 346 | /** 347 | * 移除已显示的但在当前scroll offset状态下处于屏幕外的item 348 | */ 349 | Rect childFrame = new Rect(); 350 | for (int i = 0; i < self.getChildCount(); i++) { 351 | View child = self.getChildAt(i); 352 | childFrame.left = self.getDecoratedLeft(child); 353 | childFrame.top = self.getDecoratedTop(child); 354 | childFrame.right = self.getDecoratedRight(child); 355 | childFrame.bottom = self.getDecoratedBottom(child); 356 | 357 | if (!Rect.intersects(displayFrame, childFrame)) { 358 | self.getState().itemsAttached.put(self.getPosition(child), false); 359 | self.removeAndRecycleView(child, recycler); 360 | } 361 | } 362 | 363 | /** 364 | * 摆放需要显示的item 365 | * 由于RecyclerView实际上并没有scroll,也就是说RecyclerView容器的滑动效果是依赖于LayoutManager对item进行平移来实现的 366 | * 故在放置item时要将item的计算位置平移到实际位置 367 | */ 368 | for (int i = 0; i < self.getItemCount(); i++) { 369 | if (Rect.intersects(displayFrame, self.getState().itemsFrames.get(i))) { 370 | /** 371 | * 在onLayoutChildren时由于移除了所有的item view,可以遍历全部item进行添加 372 | * 在scroll时就不同了,由于scroll时会先将已显示的item view进行平移,然后移除屏幕外的item view,此时仍然在屏幕内显示的item view就无需再次添加了 373 | */ 374 | if (!self.getState().itemsAttached.get(i)) { 375 | View scrap = recycler.getViewForPosition(i); 376 | self.measureChildWithMargins(scrap, 0, 0); 377 | self.addView(scrap); 378 | 379 | Rect frame = self.getState().itemsFrames.get(i); 380 | self.layoutDecorated(scrap, 381 | frame.left - self.getState().scrolledX, 382 | frame.top - self.getState().scrolledY, 383 | frame.right - self.getState().scrolledX, 384 | frame.bottom - self.getState().scrolledY); // Important!布局到RecyclerView容器中,所有的计算都是为了得出任意position的item的边界来布局 385 | 386 | self.getState().itemsAttached.put(i, true); 387 | } 388 | } 389 | } 390 | } 391 | 392 | 393 | /** 394 | * 记录当前LayoutManager的一些信息 395 | */ 396 | private State state; 397 | public State getState() { 398 | if (state == null) { 399 | state = new State(); 400 | } 401 | return state; 402 | } 403 | class State { 404 | /** 405 | * 存放所有item的位置和尺寸 406 | */ 407 | SparseArray itemsFrames; 408 | 409 | /** 410 | * 记录item是否已经展示 411 | */ 412 | SparseBooleanArray itemsAttached; 413 | 414 | /** 415 | * 横向滑动距离 416 | * @see #scrollHorizontallyBy(int, RecyclerView.Recycler, RecyclerView.State) 417 | */ 418 | int scrolledX; 419 | 420 | /** 421 | * 纵向滑动距离 422 | * @see #scrollVerticallyBy(int, RecyclerView.Recycler, RecyclerView.State) 423 | */ 424 | int scrolledY; 425 | 426 | /** 427 | * 内容宽度 428 | * note:最小宽度为容器宽度 429 | */ 430 | int contentWidth; 431 | 432 | /** 433 | * 内容高度 434 | * note:最小高度为容器高度 435 | */ 436 | int contentHeight; 437 | 438 | /** 439 | * 是否允许横向滑动 440 | * 默认允许 441 | */ 442 | boolean canScrollHorizontal; 443 | 444 | /** 445 | * 是否允许纵向滑动 446 | * 默认允许 447 | */ 448 | boolean canScrollVertical; 449 | 450 | /** 451 | * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL} 452 | */ 453 | int orientation; 454 | 455 | /** 456 | * {@link #orientation} 为 {@link #HORIZONTAL} 时,每列显示的item数 457 | * {@link #orientation} 为 {@link #VERTICAL} 时,每行显示的item数 458 | * 每个item有可能占用多格 {@link com.vilyever.howtocustomlayoutmanager.CustomLayoutManager.LayoutParams#occupationLineBlocks} 459 | */ 460 | int itemLineCount; 461 | 462 | /** 463 | * {@link #orientation} 为 {@link #HORIZONTAL} 时,表示列数 464 | * {@link #orientation} 为 {@link #VERTICAL} 时,表示行数 465 | */ 466 | int totalSpreadCount; 467 | 468 | public State() { 469 | itemsFrames = new SparseArray<>(); 470 | itemsAttached = new SparseBooleanArray(); 471 | scrolledX = 0; 472 | scrolledY = 0; 473 | contentWidth = 0; 474 | contentHeight = 0; 475 | canScrollHorizontal = true; 476 | canScrollVertical = true; 477 | itemLineCount = 1; 478 | totalSpreadCount = 0; 479 | } 480 | } 481 | 482 | /** 483 | * 启用/禁用横向滑动 484 | * @param canScrollHorizontal 启用/禁用 485 | * @return {@link State#canScrollHorizontal} 486 | */ 487 | public CustomLayoutManager setCanScrollHorizontal(boolean canScrollHorizontal) { 488 | self.getState().canScrollHorizontal = canScrollHorizontal; 489 | return this; 490 | } 491 | public boolean getCanScrollHorizontal() { 492 | return self.getState().canScrollHorizontal; 493 | } 494 | 495 | /** 496 | * 启用/禁用纵向滑动 497 | * @param canScrollVertical 启用/禁用 498 | * @return {@link State#canScrollVertical} 499 | */ 500 | public CustomLayoutManager setCanScrollVertical(boolean canScrollVertical) { 501 | self.getState().canScrollVertical = canScrollVertical; 502 | return this; 503 | } 504 | public boolean getCanScrollVertical() { 505 | return self.getState().canScrollVertical; 506 | } 507 | 508 | /** 509 | * 纵然此LayoutManager在水平方向和垂直方向都可以滑动 510 | * 此LayoutManager仍然带有orientation属性 511 | * orientation将影响item摆放的次序 512 | * 513 | * 若Direction为Vertical,item的摆放顺序为从左到右,一行铺满后填充下一行 514 | * 若Direction为Horizontal,item的摆放顺序为从上到下,一列铺满后填充下一列 515 | */ 516 | @IntDef({HORIZONTAL, VERTICAL}) 517 | @Retention(RetentionPolicy.SOURCE) 518 | public @interface Orientation {} 519 | public static final int HORIZONTAL = LinearLayout.HORIZONTAL; 520 | public static final int VERTICAL = LinearLayout.VERTICAL; 521 | public CustomLayoutManager setOrientation(@Orientation int orientation) { 522 | if (orientation != HORIZONTAL && orientation != VERTICAL) { 523 | throw new IllegalArgumentException("invalid orientation:" + orientation); 524 | } 525 | assertNotInLayoutOrScroll(null); 526 | if (orientation == self.getState().orientation) { 527 | return this; 528 | } 529 | self.getState().orientation = orientation; 530 | self.requestLayout(); 531 | return this; 532 | } 533 | @Orientation 534 | public int getOrientation() { 535 | return self.getState().orientation; 536 | } 537 | 538 | public CustomLayoutManager setItemLineCount(int itemLineCount) { 539 | assertNotInLayoutOrScroll(null); 540 | if (itemLineCount == self.getState().itemLineCount) { 541 | return this; 542 | } 543 | self.getState().itemLineCount = itemLineCount; 544 | self.requestLayout(); 545 | return this; 546 | } 547 | public int getItemLineCount() { 548 | return self.getState().itemLineCount; 549 | } 550 | 551 | /** 552 | * 依照内容宽高调整记录的滑动距离,防止滑动偏移过大 553 | */ 554 | private void fixScrollOffset() { 555 | if (self.getState().contentWidth == self.getHorizontalSpace()) { 556 | self.getState().scrolledX = 0; 557 | } 558 | if (self.getState().scrolledX > (self.getState().contentWidth - self.getHorizontalSpace())) { 559 | self.getState().scrolledX = self.getState().contentWidth - self.getHorizontalSpace(); 560 | } 561 | if (self.getState().contentHeight == self.getVerticalSpace()) { 562 | self.getState().scrolledY = 0; 563 | } 564 | if (self.getState().scrolledY > (self.getState().contentHeight - self.getVerticalSpace())) { 565 | self.getState().scrolledY = self.getState().contentHeight - self.getVerticalSpace(); 566 | } 567 | } 568 | 569 | /** 570 | * 容器去除padding后的宽度 571 | * @return 实际可摆放item的空间 572 | */ 573 | private int getHorizontalSpace() { 574 | return self.getWidth() - self.getPaddingRight() - self.getPaddingLeft(); 575 | } 576 | 577 | /** 578 | * 容器去除padding后的高度 579 | * @return 实际可摆放item的空间 580 | */ 581 | private int getVerticalSpace() { 582 | return self.getHeight() - self.getPaddingBottom() - self.getPaddingTop(); 583 | } 584 | } -------------------------------------------------------------------------------- /app/src/main/java/com/vilyever/howtocustomlayoutmanager/DataAdapter.java: -------------------------------------------------------------------------------- 1 | package com.vilyever.howtocustomlayoutmanager; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.ViewGroup; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | /** 10 | * DataAdapter 11 | * HowToCustomLayoutManager 12 | * Created by vilyever on 2016/1/7. 13 | * Feature: 14 | * 示例适配器 15 | * 通过绑定数据列表向绑定的ViewHolder提供内容 16 | */ 17 | public class DataAdapter extends RecyclerView.Adapter implements DemoViewHolder.Datasource, DemoViewHolder.Delegate { 18 | /** Convenience Var to call this */ 19 | final DataAdapter self = this; 20 | 21 | /** @see RecyclerView.Adapter#onCreateViewHolder(ViewGroup, int) **/ 22 | @Override 23 | public DemoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 24 | return DemoViewHolder.newInstance(parent); 25 | } 26 | 27 | /** @see RecyclerView.Adapter#onBindViewHolder(RecyclerView.ViewHolder, int) **/ 28 | @Override 29 | public void onBindViewHolder(DemoViewHolder holder, int position) { 30 | holder.itemView.getLayoutParams().width = self.getDemoModels().get(position).getPreferWidth(); 31 | holder.itemView.getLayoutParams().height = self.getDemoModels().get(position).getPreferHeight(); 32 | ((CustomLayoutManager.LayoutParams) holder.itemView.getLayoutParams()).occupationLineBlocks = self.getDemoModels().get(position).getBlocks(); 33 | holder.setDelegate(self); 34 | holder.reload(self); 35 | } 36 | 37 | /** @see RecyclerView.Adapter#getItemCount() **/ 38 | @Override 39 | public int getItemCount() { 40 | return self.getDemoModels().size(); 41 | } 42 | 43 | /** {@link com.vilyever.howtocustomlayoutmanager.DemoViewHolder.Datasource} **/ 44 | @Override 45 | public String gainTitle(DemoViewHolder viewHolder) { 46 | return self.getDemoModels().get(viewHolder.getAdapterPosition()).getTitle(); 47 | } 48 | @Override 49 | public int gainBackgroundColor(DemoViewHolder viewHolder) { 50 | return self.getDemoModels().get(viewHolder.getAdapterPosition()).getColor(); 51 | } 52 | /** {@link com.vilyever.howtocustomlayoutmanager.DemoViewHolder.Datasource} **/ 53 | 54 | /** {@link com.vilyever.howtocustomlayoutmanager.DemoViewHolder.Delegate} **/ 55 | @Override 56 | public void onClick(DemoViewHolder viewHolder) { 57 | self.getDelegate().onItemClick(self, viewHolder.getAdapterPosition()); 58 | } 59 | /** {@link com.vilyever.howtocustomlayoutmanager.DemoViewHolder.Delegate} **/ 60 | 61 | private Delegate delegate; 62 | public DataAdapter setDelegate(Delegate delegate) { 63 | this.delegate = delegate; 64 | return this; 65 | } 66 | public Delegate getDelegate() { 67 | if (delegate == null) { 68 | return NullDelegate; 69 | } 70 | return delegate; 71 | } 72 | public interface Delegate { 73 | void onItemClick(DataAdapter adapter, int position); 74 | } 75 | private static final Delegate NullDelegate = new Delegate() { 76 | @Override 77 | public void onItemClick(DataAdapter adapter, int position) { 78 | } 79 | }; 80 | 81 | /** 82 | * 绑定的数据列表 83 | */ 84 | private List demoModels; 85 | public List getDemoModels() { 86 | if (demoModels == null) { 87 | demoModels = new ArrayList<>(); 88 | } 89 | return demoModels; 90 | } 91 | public DataAdapter setDemoModels(List demoModels) { 92 | this.demoModels = demoModels; 93 | // self.notifyDataSetChanged(); 94 | self.notifyItemRangeChanged(0, demoModels.size()); 95 | return self; 96 | } 97 | 98 | public DataAdapter addModel(int position, DemoModel demoModel) { 99 | if (position < 0 || position > self.getDemoModels().size()) { 100 | throw new IndexOutOfBoundsException("position should between " + 0 + "~" + self.getDemoModels().size()); 101 | } 102 | self.getDemoModels().add(position, demoModel); 103 | self.notifyItemInserted(position); 104 | return self; 105 | } 106 | 107 | public DataAdapter removeModel(int position) { 108 | if (position < 0 || position > self.getDemoModels().size()) { 109 | throw new IndexOutOfBoundsException("position should between " + 0 + "~" + (self.getDemoModels().size() - 1)); 110 | } 111 | self.getDemoModels().remove(position); 112 | self.notifyItemRemoved(position); 113 | return self; 114 | } 115 | 116 | public DataAdapter updateModel(int position, DemoModel demoModel) { 117 | self.getDemoModels().set(position, demoModel); 118 | self.notifyItemChanged(position); 119 | return self; 120 | } 121 | 122 | public DataAdapter clear() { 123 | self.getDemoModels().clear(); 124 | self.notifyDataSetChanged(); 125 | return self; 126 | } 127 | 128 | } -------------------------------------------------------------------------------- /app/src/main/java/com/vilyever/howtocustomlayoutmanager/DemoModel.java: -------------------------------------------------------------------------------- 1 | package com.vilyever.howtocustomlayoutmanager; 2 | 3 | import android.graphics.Color; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Random; 7 | 8 | /** 9 | * DemoModel 10 | * HowToCustomLayoutManager 11 | * Created by vilyever on 2016/1/7. 12 | * Feature: 13 | * 示例model 14 | * 仅提供示例所需数据,实际应用中可能存在更多冗余数据(针对RecyclerView所需) 15 | */ 16 | public class DemoModel { 17 | /** Convenience Var to call this */ 18 | final DemoModel self = this; 19 | 20 | public static DemoModel generateDemoModel() { 21 | DemoModel demoModel = new DemoModel(); 22 | Random random = new Random(); 23 | int color = Color.rgb(Math.abs(random.nextInt()) % 256, Math.abs(random.nextInt()) % 256, Math.abs(random.nextInt()) % 256); 24 | int width = (Math.abs(random.nextInt()) % 6 + 1) * 80 + 100; 25 | int height = (Math.abs(random.nextInt()) % 6 + 1) * 80 + 100; 26 | int block = Math.abs(random.nextInt()) % 6; 27 | demoModel.setTitle(width + "x" + height + "(" + block + ")").setColor(color).setPreferWidth(width).setPreferHeight(height).setBlocks(block); 28 | return demoModel; 29 | } 30 | 31 | /** 32 | * 生成50个model列表 33 | * @return 50个model列表 34 | */ 35 | public static ArrayList generateDemoList() { 36 | ArrayList models = new ArrayList<>(); 37 | 38 | for (int i = 0; i < 50; i++) { 39 | models.add(DemoModel.generateDemoModel()); 40 | } 41 | 42 | return models; 43 | } 44 | 45 | private String title; 46 | public DemoModel setTitle(String title) { 47 | this.title = title; 48 | return this; 49 | } 50 | public String getTitle() { 51 | return title; 52 | } 53 | 54 | private int color; 55 | public DemoModel setColor(int color) { 56 | this.color = color; 57 | return this; 58 | } 59 | public int getColor() { 60 | return color; 61 | } 62 | 63 | private int preferWidth; 64 | public DemoModel setPreferWidth(int preferWidth) { 65 | this.preferWidth = preferWidth; 66 | return this; 67 | } 68 | public int getPreferWidth() { 69 | return preferWidth; 70 | } 71 | 72 | private int preferHeight; 73 | public DemoModel setPreferHeight(int preferHeight) { 74 | this.preferHeight = preferHeight; 75 | return this; 76 | } 77 | public int getPreferHeight() { 78 | return preferHeight; 79 | } 80 | 81 | private int blocks; 82 | public DemoModel setBlocks(int blocks) { 83 | this.blocks = blocks; 84 | return this; 85 | } 86 | public int getBlocks() { 87 | return blocks; 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /app/src/main/java/com/vilyever/howtocustomlayoutmanager/DemoViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.vilyever.howtocustomlayoutmanager; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.TextView; 8 | 9 | /** 10 | * DemoViewHolder 11 | * HowToCustomLayoutManager 12 | * Created by vilyever on 2016/1/7. 13 | * Feature: 14 | * 示例ViewHolder,简单展示一个TextView显示不同文字和背景色 15 | */ 16 | public class DemoViewHolder extends RecyclerView.ViewHolder { 17 | /** Convenience Var to call this */ 18 | final DemoViewHolder self = this; 19 | 20 | public DemoViewHolder(View itemView) { 21 | super(itemView); 22 | 23 | itemView.setOnClickListener(new View.OnClickListener() { 24 | @Override 25 | public void onClick(View v) { 26 | self.getDelegate().onClick(self); 27 | } 28 | }); 29 | } 30 | 31 | /** 32 | * itemView点击事件回调接口 33 | */ 34 | private Delegate delegate; 35 | public DemoViewHolder setDelegate(Delegate delegate) { 36 | this.delegate = delegate; 37 | return this; 38 | } 39 | public Delegate getDelegate() { 40 | if (delegate == null) { 41 | return NullDelegate; 42 | } 43 | return delegate; 44 | } 45 | public interface Delegate { 46 | void onClick(DemoViewHolder viewHolder); 47 | } 48 | private static final Delegate NullDelegate = new Delegate() { 49 | @Override 50 | public void onClick(DemoViewHolder viewHolder) { 51 | } 52 | }; 53 | 54 | /** 55 | * 文字标签,用于展示文字和背景色 56 | * 可以通过getter{@link #getTitleLabel()}直接在adapter中获取 57 | */ 58 | private TextView titleLabel; 59 | public TextView getTitleLabel() { if (titleLabel == null) { titleLabel = (TextView) self.itemView.findViewById(R.id.titleLabel); } return titleLabel; } 60 | 61 | /** 62 | * 刷新此ViewHolder 63 | * 纵然{@link #titleLabel} 控件可以通过getter{@link #getTitleLabel()}可以直接在外部获取 64 | * 通过{@link Datasource}数据接口可使此ViewHolder专注于自己的业务逻辑,而非在adapter中任意更改 65 | * @param datasource 数据源 66 | */ 67 | public void reload(@NonNull Datasource datasource) { 68 | self.getTitleLabel().setText(datasource.gainTitle(self)); 69 | self.getTitleLabel().setBackgroundColor(datasource.gainBackgroundColor(self)); 70 | } 71 | 72 | /** 73 | * {@link DemoViewHolder} 便捷构造,adapter无需关心ViewHolder的layout文件是什么 74 | * @param parent itemView的父view 75 | * @return {@link DemoViewHolder}实例 76 | */ 77 | public static DemoViewHolder newInstance(ViewGroup parent) { 78 | Class currentClass = new Object() { }.getClass().getEnclosingClass(); // 获取当前class,免除每次攥写ViewHolder都要改名获取当前ViewHolder类型的麻烦 79 | return ViewHolderCreator.CreateInstance(currentClass, parent, R.layout.demo_view_holder); 80 | } 81 | 82 | /** 83 | * 数据接口,更新此ViewHolder只需要提供以下数据 84 | */ 85 | public interface Datasource { 86 | String gainTitle(DemoViewHolder viewHolder); 87 | int gainBackgroundColor(DemoViewHolder viewHolder); 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/com/vilyever/howtocustomlayoutmanager/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.vilyever.howtocustomlayoutmanager; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.Snackbar; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.support.v7.widget.Toolbar; 8 | import android.view.Menu; 9 | import android.view.MenuItem; 10 | 11 | import java.util.ArrayList; 12 | 13 | public class MainActivity extends AppCompatActivity { 14 | /** Convenience Var to call this */ 15 | final MainActivity self = this; 16 | 17 | private RecyclerView recyclerView; 18 | public RecyclerView getRecyclerView() { if (recyclerView == null) { recyclerView = (RecyclerView) self.findViewById(R.id.recyclerView); } return recyclerView; } 19 | 20 | private CustomLayoutManager layoutManager; 21 | public CustomLayoutManager getLayoutManager() { 22 | if (layoutManager == null) { 23 | layoutManager = new CustomLayoutManager(CustomLayoutManager.HORIZONTAL, 5); 24 | } 25 | return layoutManager; 26 | } 27 | 28 | private ArrayList demoModels; 29 | public ArrayList getDemoModels() { 30 | if (demoModels == null) { 31 | demoModels = DemoModel.generateDemoList(); 32 | } 33 | return demoModels; 34 | } 35 | 36 | private DataAdapter dataAdapter; 37 | public DataAdapter getDataAdapter() { 38 | if (dataAdapter == null) { 39 | dataAdapter = new DataAdapter(); 40 | dataAdapter.setDelegate(new DataAdapter.Delegate() { 41 | @Override 42 | public void onItemClick(DataAdapter adapter, int position) { 43 | Snackbar.make(self.getRecyclerView(), 44 | "Item " + position + " clicked. " + self.getDataAdapter().getDemoModels().get(position).getPreferWidth() + "x" + self.getDataAdapter().getDemoModels().get(position).getPreferHeight(), 45 | Snackbar.LENGTH_LONG) 46 | .setAction("Action", null).show(); 47 | 48 | self.getDataAdapter().updateModel(position, DemoModel.generateDemoModel()); 49 | 50 | self.getLayoutManager().setCanScrollHorizontal(false); 51 | } 52 | }); 53 | } 54 | return dataAdapter; 55 | } 56 | 57 | @Override 58 | protected void onCreate(Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | setContentView(R.layout.activity_main); 61 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 62 | setSupportActionBar(toolbar); 63 | 64 | 65 | self.getRecyclerView().setLayoutManager(self.getLayoutManager()); 66 | self.getRecyclerView().setAdapter(self.getDataAdapter()); 67 | self.getDataAdapter().setDemoModels(self.getDemoModels()); 68 | self.getRecyclerView().getItemAnimator().setAddDuration(500); 69 | self.getRecyclerView().getItemAnimator().setChangeDuration(500); 70 | self.getRecyclerView().getItemAnimator().setMoveDuration(500); 71 | self.getRecyclerView().getItemAnimator().setRemoveDuration(500); 72 | } 73 | 74 | @Override 75 | public boolean onCreateOptionsMenu(Menu menu) { 76 | // Inflate the menu; this adds items to the action bar if it is present. 77 | getMenuInflater().inflate(R.menu.menu_main, menu); 78 | return true; 79 | } 80 | 81 | @Override 82 | public boolean onOptionsItemSelected(MenuItem item) { 83 | // Handle action bar item clicks here. The action bar will 84 | // automatically handle clicks on the Home/Up button, so long 85 | // as you specify a parent activity in AndroidManifest.xml. 86 | int id = item.getItemId(); 87 | 88 | /** 89 | * copy the NumberPickerDialog from https://github.com/devunwired/recyclerview-playground/blob/master/app/src/main/java/com/example/android/recyclerplayground/NumberPickerDialog.java 90 | */ 91 | NumberPickerDialog dialog; 92 | switch (item.getItemId()) { 93 | case R.id.action_add: 94 | dialog = new NumberPickerDialog(self); 95 | dialog.setTitle("Position to Add"); 96 | dialog.setPickerRange(0, self.getDataAdapter().getItemCount()); 97 | dialog.setOnNumberSelectedListener(new NumberPickerDialog.OnNumberSelectedListener() { 98 | @Override 99 | public void onNumberSelected(int value) { 100 | self.getDataAdapter().addModel(value, DemoModel.generateDemoModel()); 101 | } 102 | }); 103 | dialog.show(); 104 | 105 | return true; 106 | case R.id.action_remove: 107 | dialog = new NumberPickerDialog(self); 108 | dialog.setTitle("Position to Remove"); 109 | dialog.setPickerRange(0, self.getDataAdapter().getItemCount() - 1); 110 | dialog.setOnNumberSelectedListener(new NumberPickerDialog.OnNumberSelectedListener() { 111 | @Override 112 | public void onNumberSelected(int value) { 113 | self.getDataAdapter().removeModel(value); 114 | } 115 | }); 116 | dialog.show(); 117 | 118 | return true; 119 | case R.id.action_empty: 120 | self.getDataAdapter().clear(); 121 | return true; 122 | case R.id.action_small: 123 | self.getDemoModels().clear(); 124 | for (int i = 0; i < 5; i++) { 125 | self.getDemoModels().add(DemoModel.generateDemoModel()); 126 | } 127 | self.getDataAdapter().setDemoModels(self.getDemoModels()); 128 | return true; 129 | case R.id.action_medium: 130 | self.getDemoModels().clear(); 131 | for (int i = 0; i < 50; i++) { 132 | self.getDemoModels().add(DemoModel.generateDemoModel()); 133 | } 134 | self.getDataAdapter().setDemoModels(self.getDemoModels()); 135 | return true; 136 | case R.id.action_large: 137 | self.getDemoModels().clear(); 138 | for (int i = 0; i < 100; i++) { 139 | self.getDemoModels().add(DemoModel.generateDemoModel()); 140 | } 141 | self.getDataAdapter().setDemoModels(self.getDemoModels()); 142 | return true; 143 | case R.id.action_scroll_to_position: 144 | dialog = new NumberPickerDialog(self); 145 | dialog.setTitle("Position to Scroll"); 146 | dialog.setPickerRange(0, self.getDataAdapter().getItemCount() - 1); 147 | dialog.setOnNumberSelectedListener(new NumberPickerDialog.OnNumberSelectedListener() { 148 | @Override 149 | public void onNumberSelected(int value) { 150 | self.getRecyclerView().scrollToPosition(value); 151 | } 152 | }); 153 | dialog.show(); 154 | 155 | return true; 156 | case R.id.action_smooth_to_position: 157 | dialog = new NumberPickerDialog(self); 158 | dialog.setTitle("Position to Smooth Scroll"); 159 | dialog.setPickerRange(0, self.getDataAdapter().getItemCount() - 1); 160 | dialog.setOnNumberSelectedListener(new NumberPickerDialog.OnNumberSelectedListener() { 161 | @Override 162 | public void onNumberSelected(int value) { 163 | self.getRecyclerView().smoothScrollToPosition(value); 164 | } 165 | }); 166 | dialog.show(); 167 | return true; 168 | default: 169 | return super.onOptionsItemSelected(item); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /app/src/main/java/com/vilyever/howtocustomlayoutmanager/NumberPickerDialog.java: -------------------------------------------------------------------------------- 1 | package com.vilyever.howtocustomlayoutmanager; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.os.Bundle; 7 | import android.widget.NumberPicker; 8 | 9 | /** 10 | * copy from https://github.com/devunwired/recyclerview-playground/blob/master/app/src/main/java/com/example/android/recyclerplayground/NumberPickerDialog.java 11 | */ 12 | public class NumberPickerDialog extends AlertDialog implements DialogInterface.OnClickListener { 13 | 14 | public interface OnNumberSelectedListener { 15 | void onNumberSelected(int value); 16 | } 17 | 18 | private NumberPicker mPicker; 19 | private OnNumberSelectedListener mNumberSelectedListener; 20 | 21 | public NumberPickerDialog(Context context) { 22 | super(context); 23 | mPicker = new NumberPicker(context); 24 | mPicker.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); 25 | } 26 | 27 | protected NumberPickerDialog(Context context, int theme) { 28 | super(context, theme); 29 | } 30 | 31 | protected NumberPickerDialog(Context context, boolean cancelable, OnCancelListener cancelListener) { 32 | super(context, cancelable, cancelListener); 33 | } 34 | 35 | @Override 36 | protected void onCreate(Bundle savedInstanceState) { 37 | setButton(BUTTON_NEGATIVE, getContext().getString(android.R.string.cancel), this); 38 | setButton(BUTTON_POSITIVE, getContext().getString(android.R.string.ok), this); 39 | setView(mPicker); 40 | 41 | //Install contents 42 | super.onCreate(savedInstanceState); 43 | } 44 | 45 | public void setOnNumberSelectedListener(OnNumberSelectedListener listener) { 46 | mNumberSelectedListener = listener; 47 | } 48 | 49 | public void setPickerRange(int minValue, int maxValue) { 50 | mPicker.setMinValue(minValue); 51 | mPicker.setMaxValue(maxValue); 52 | } 53 | 54 | @Override 55 | public void onClick(DialogInterface dialog, int which) { 56 | if (which == BUTTON_POSITIVE && mNumberSelectedListener != null) { 57 | mNumberSelectedListener.onNumberSelected(mPicker.getValue()); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/vilyever/howtocustomlayoutmanager/ViewHolderCreator.java: -------------------------------------------------------------------------------- 1 | package com.vilyever.howtocustomlayoutmanager; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import java.lang.reflect.Constructor; 9 | 10 | /** 11 | * ViewHolderCreator 12 | * HowToCustomLayoutManager 13 | * Created by vilyever on 2016/1/7. 14 | * Feature: 15 | * 提供ViewHolder的便捷构造 16 | */ 17 | public class ViewHolderCreator { 18 | /** Convenience Var to call this */ 19 | final ViewHolderCreator self = this; 20 | 21 | /** 22 | * ViewHolder便捷构造 23 | * @param clazz 需要生成的ViewHolder的class 24 | * @param parent 生成ViewHolder的itemView的父view 25 | * @param layoutID 生成ViewHolder的itemView的layout 26 | * @param 泛型继承{@link android.support.v7.widget.RecyclerView.ViewHolder} 27 | * @return 类型为T的实例 28 | */ 29 | @SuppressWarnings("unchecked") 30 | public static T CreateInstance(Class clazz, ViewGroup parent, int layoutID) { 31 | View itemView = LayoutInflater.from(parent.getContext()) 32 | .inflate(layoutID, parent, false); 33 | 34 | try { 35 | Constructor constructor = clazz.getDeclaredConstructor(View.class); 36 | constructor.setAccessible(true); 37 | return (T) constructor.newInstance(itemView); 38 | } 39 | catch (Exception e) { 40 | e.printStackTrace(); 41 | } 42 | 43 | return null; 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/demo_view_holder.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 8 | 11 | 12 | 16 | 20 | 24 | 28 | 29 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilyever/HowToCustomLayoutManager/f1ffa4f79df0386ea884c9b577e673b9611471ab/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilyever/HowToCustomLayoutManager/f1ffa4f79df0386ea884c9b577e673b9611471ab/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilyever/HowToCustomLayoutManager/f1ffa4f79df0386ea884c9b577e673b9611471ab/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilyever/HowToCustomLayoutManager/f1ffa4f79df0386ea884c9b577e673b9611471ab/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilyever/HowToCustomLayoutManager/f1ffa4f79df0386ea884c9b577e673b9611471ab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | > 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | HowToCustomLayoutManager 3 | Settings 4 | 5 | Add 6 | Remove 7 | Empty 8 | Small Data 9 | Medium Data 10 | Large Data 11 | Scroll Vertical Zero 12 | Scroll Horizontal Zero 13 | Scroll Vertical End 14 | Scroll Horizontal End 15 | Smooth Scroll Vertical Zero 16 | Smooth Scroll Vertical End 17 | Smooth Scroll Horizontal Zero 18 | Smooth Scroll Horizontal End 19 | Scroll To Position 20 | Smooth Scroll To Position 21 | Main2Activity 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |