├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── dthfish │ │ └── scrollviewindicator │ │ ├── MainActivity.java │ │ ├── ScrollViewTabIndicator.java │ │ └── SecondActivity.java │ └── res │ ├── color │ └── color_666666_to_d30775_selector.xml │ ├── layout │ ├── activity_main.xml │ └── activity_second.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea 4 | .idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | *.iml 9 | /app/hash.txt 10 | /.repo 11 | /node_modules/ 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScrollViewIndicator 2 | a tab indicator for NestedScrollView 3 | 4 | 5 | ### 一、思路 6 | 7 | 现在很多应用都采用 ViewPager 加 Fragment 的结构,在 github 上随便一搜也可以找出各种各样的动画效果的 ViewPagerIndicator。前不久在项目详情页改版的需求中,需要把原来的 ViewPager 切换的结构修改成垂直滚动的结构(如下图)。 8 | 9 | 10 | ![scrollviewindicator1.gif](http://upload-images.jianshu.io/upload_images/5463583-99d80feefb15541f.gif?imageMogr2/auto-orient/strip) 11 | 12 | 13 | 第一个反应就是把原来的 ViewPagerIndicator 替换成 RadioGroup 和 RadioButton 然后设置监听,但是又不想放弃原来的 ViewPagerIndicator 的 tab 的切换动画效果。 14 | 15 | 然后我选择了第二种方法——在原来的 NestedScrollView 包含的子 ViewGroup 中插入一个宽为 match_parent,高为 1px 的 ViewPager,起到辅助动画的功能,来与 NestedScrollView 联动达到上图的效果。 16 | 17 | ### 二、效果 18 | 讲完了思路,先来看下最终实现的效果,效果图就是上边这张,这里主要是给大家看下代码里如何使用,使用是否方便。 19 | ``` java 20 | public class MainActivity extends AppCompatActivity implements NestedScrollView.OnScrollChangeListener{ 21 | 22 | private NestedScrollView mSv; 23 | private ScrollViewTabIndicator mTab; 24 | private ScrollViewTabIndicator mTab2; 25 | private int[] mTabMiddleLocation = new int[2]; 26 | private int[] mTabTopLocation = new int[2]; 27 | 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.activity_main); 32 | initView(); 33 | } 34 | 35 | private void initView() { 36 | mSv = (NestedScrollView) findViewById(R.id.sv); 37 | mTab = (ScrollViewTabIndicator) findViewById(R.id.tab);//在TitleBar下方的indicator 38 | mTab2 = (ScrollViewTabIndicator) findViewById(R.id.tab2);//在ScrollView中的indicator 39 | View view1 = findViewById(R.id.tv_1);//详情View 40 | View view2 = findViewById(R.id.tv_2);//评论View 41 | View view3 = findViewById(R.id.tv_3);//须知View 42 | List names = new ArrayList<>(); 43 | names.add("详情"); 44 | names.add("评论"); 45 | names.add("须知"); 46 | List views = new ArrayList<>(); 47 | views.add(view1); 48 | views.add(view2); 49 | views.add(view3); 50 | mTab.setScrollView(mSv,this,names,views); 51 | //将mTab本身作为参数传入mTab2已达到同步状态 52 | mTab2.setScrollView(mSv,mTab,names,views); 53 | } 54 | 55 | @Override 56 | public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { 57 | setVisibleAndGone(); 58 | } 59 | 60 | private void setVisibleAndGone() { 61 | mTab2.getLocationOnScreen(mTabMiddleLocation); 62 | mTab.getLocationOnScreen(mTabTopLocation); 63 | if (mTabMiddleLocation[1] <= mTabTopLocation[1]) { 64 | mTab.setVisibility(View.VISIBLE); 65 | mTab2.setVisibility(View.INVISIBLE); 66 | } else { 67 | mTab.setVisibility(View.INVISIBLE); 68 | mTab2.setVisibility(View.VISIBLE); 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | 可以看到使用的方法仅仅是找出 ScrollView 中对应的 View,并给出对应的 tab 标题,然后调用 setScrollView 方法设置到 ScrollViewTabIndicator,其余的事都交给 ScrollViewTabIndicator 来执行,唯一要自己处理的就是监听滚动来控制 mTab 和 mTab2 的显示和隐藏。 75 | ### 三、封装 76 | 当然这里我将很多 ViewPager 和 ScrollView 的逻辑都封装起来了,否则你会发现的 Activity 或者 Fragment 中你会发现要增加很多与业务无关的代码,而且也不利于后期的复用。下边我就介绍下基于 ViewPagerIndicator 的一些修改。 77 | 78 | ##### 3.1 设置逻辑 79 | ``` java 80 | /** 81 | * 因为会替换 scrollview 上的 listener, 所以要传进来. 82 | * 如果传进来是一个 TabIndicator 对象, 则两者状态会同步, 并且自定义的滚动监听要设置到第一个上边 83 | * @param scrollView 监听的 NestedScrollView 84 | * @param listener 原先设置在 NestedScrollView 上的监听 85 | * @param tabs tab 的标题 86 | * @param views 各个 tab 对应的需要滚动到的 View 87 | */ 88 | public void setScrollView(NestedScrollView scrollView, NestedScrollView.OnScrollChangeListener listener, List tabs, List views) { 89 | if (mScrollView == scrollView) { 90 | return; 91 | } 92 | if (tabs == null || views == null) { 93 | throw new IllegalArgumentException("tabs and views should not be null!"); 94 | } 95 | if (tabs.isEmpty() || views.isEmpty()) { 96 | throw new IllegalArgumentException("tabs and views should not be empty!"); 97 | } 98 | if (tabs.size() != views.size()) { 99 | throw new IllegalArgumentException("tabs and views should be the same length!"); 100 | } 101 | mScrollListener = listener; 102 | mScrollView = scrollView; 103 | mViews = views; 104 | if (mScrollView != null) { 105 | mScrollView.setOnScrollChangeListener(this); 106 | } 107 | initTabs(tabs); 108 | if (listener instanceof ScrollViewTabIndicator) { 109 | ScrollViewTabIndicator synchronize = (ScrollViewTabIndicator) listener; 110 | mAssistViewPager = synchronize.getAssistViewPager(); 111 | //接收覆盖监听,避免走多余的监听流程 112 | mScrollListener = synchronize.mScrollListener; 113 | if (mAssistViewPager != null) { 114 | mAssistViewPager.addOnPageChangeListener(this); 115 | } else { 116 | initAssistViewPager(tabs.size()); 117 | } 118 | } else { 119 | initAssistViewPager(tabs.size()); 120 | } 121 | } 122 | ``` 123 | 124 | 去除了原来的具备的 setViewPager 方法,添加了 setScrollView,这里主要就是进行各种判空,接收传进来的参数(listener),并调用 initTabs(tabs) 来生成对应的 tab。之后调用 initAssistViewPager(tabs.size()) 来创建辅助动画的 ViewPager,可以先不管 if() 里面的代码。 125 | 126 | ##### 3.2 辅助ViewPager 127 | 128 | ``` java 129 | private void initAssistViewPager(int size) { 130 | if (mAssistViewPager != null) { 131 | return; 132 | } 133 | mAssistViewPager = new ViewPager(getContext()); 134 | ViewGroup.LayoutParams p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1); 135 | mAssistViewPager.setLayoutParams(p); 136 | 137 | //请注意这段代码, 因为会在 ScrollView 的子 view 中插入一个 ViewPager 138 | View viewGroup = mScrollView.getChildAt(0); 139 | if (viewGroup == null) { 140 | throw new IllegalStateException(" The child view of the ScrollView must be not null!"); 141 | } 142 | if (!(viewGroup instanceof ViewGroup)) { 143 | throw new IllegalStateException(" The child view of the ScrollView must be a ViewGroup!"); 144 | } 145 | viewGroup = mScrollView.getChildAt(0); 146 | ((ViewGroup) viewGroup).addView(mAssistViewPager); 147 | 148 | 149 | final List viewList = new ArrayList<>(); 150 | for (int i = 0; i < size; i++) { 151 | viewList.add(new Space(getContext())); 152 | } 153 | mAssistViewPager.setAdapter(new PagerAdapter() { 154 | @Override 155 | public int getCount() { 156 | return viewList.size(); 157 | } 158 | 159 | @Override 160 | public boolean isViewFromObject(View view, Object object) { 161 | return view == object; 162 | } 163 | 164 | @Override 165 | public Object instantiateItem(ViewGroup container, int position) { 166 | container.addView(viewList.get(position)); 167 | return viewList.get(position); 168 | } 169 | 170 | @Override 171 | public void destroyItem(ViewGroup container, int position, Object object) { 172 | container.removeView(viewList.get(position)); 173 | } 174 | }); 175 | mAssistViewPager.addOnPageChangeListener(this); 176 | } 177 | ``` 178 | 创建了一个只有 1px 高度的 ViewPager 但是由于 ScrollViewTabIndicator 本身继承的是一个水平方向的 LinearLayout,而且需要给予 ViewPager 一定宽度以保证 tab 切换有一定动画效果,所以这里只能在 ScrollView 的子 view 中插入 ViewPager。并且这个 ViewPager 仅仅是为了可以在它的 page 切换的时候在它的 OnPageChangeListener 中实现 tab 切换的动画。ps:到这里我觉得针对任何一个 ViewPagerIndicator 都可以采用这个形式来修改成我所谓的 ScrollViewTabIndicator。 179 | ##### 3.3 对需要定位的 Views 在 onScrollChange 中的处理 180 | ``` java 181 | @Override 182 | public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { 183 | 184 | if (mScrollListener != null) { 185 | mScrollListener.onScrollChange(v, scrollX, scrollY, oldScrollX, oldScrollY); 186 | } 187 | 188 | if (isScrolling()) { 189 | return; 190 | } 191 | 192 | if (mStatusBarHeight == 0) { 193 | mStatusBarHeight = getBarHeight(); 194 | } 195 | int top = mActionBarHeight + getMeasuredHeight() + mStatusBarHeight;//TitleBar 高度 + 控件高度 + StatusBar 高度 196 | List locations = new ArrayList<>(); 197 | for (int i = 0, size = mViews.size(); i < size; i++) { 198 | locations.add(getViewLocation(i)); 199 | } 200 | Collections.sort(locations); 201 | int position = 0; 202 | 203 | if (top < locations.get(0)) { 204 | position = 0; 205 | } else { 206 | for (int j = 0, size = locations.size(); j < size; j++) { 207 | if (j + 1 == size) { 208 | position = j; 209 | break; 210 | } 211 | if (top >= locations.get(j) && top < locations.get(j + 1)) { 212 | //如果已经不能向下滚动了就 213 | if (!v.canScrollVertically(VERTICAL)) { 214 | position = size - 1; 215 | break; 216 | } 217 | 218 | position = j; 219 | break; 220 | } 221 | } 222 | } 223 | if (getCurrentIndex() == position) { 224 | return; 225 | } 226 | mAssistViewPager.setCurrentItem(position, true); 227 | } 228 | ``` 229 | 首先这里的 mScrollListener 就是我们在「1」中 setScrollView 里面传入的 listener;isScrolling()是 ViewPager 是否在滚动;然后给个需要定位的 View 计算在屏幕上的 y 坐标,并进行从小到大排序。 230 | ``` java 231 | private int getViewLocation(int position) { 232 | if (mViews != null && mViews.size() > position) { 233 | View view = mViews.get(position); 234 | if (view != null) { 235 | int[] location = new int[2]; 236 | view.getLocationOnScreen(location); 237 | return location[1]; 238 | } 239 | } 240 | return 0; 241 | } 242 | ``` 243 | 之后进行判断来确定是否需要切换 tab: 244 | 1.如果所有的坐标都大于 top (TitleBar 高度 + 控件高度 + StatusBar 高度,因为我们要做到有悬浮在标题栏下方的视觉效果所以这里要加控件高度,大家可自行根据需求修改这里),则 position 为 0; 245 | 2.如果存在坐标小于 top: 246 | 3. 存在 top 介于坐标 j 和 坐标 j + 1 之间,且可以继续向下滚,则取 position 为 j; 247 | 4. 存在 top 介于坐标 j 和 坐标 j + 1 之间,且不可以向下滚,取 position 为 size - 1;(为了解决最底部 View 过短永远也滚不到的情况) 248 | 5. 如果所有坐标都大于 top,则取 position 为 size - 1; 249 | 250 | 如果取到的 position 和 之前的不同则让 ViewPager 滚到新的一页,并且 tabs 进行相应的切换,当然之后的动画逻辑其实是原来 ViewPagerIndicator 的代码,这里就不进行说明,有兴趣的可以之后看下完整的代码。 251 | ##### 3.4 点击 Tab 实现切换和滚动 252 | ``` java 253 | @Override 254 | public void onClick(android.view.View v) { 255 | int position = (Integer) v.getTag(); 256 | if (mScrollView != null) { 257 | int location; 258 | location = getViewLocation(position); 259 | // 待滑动距离 = 当前坐标 - (ActionBar高度) - indicator高度 - 状态栏高度 260 | if (mStatusBarHeight == 0) { 261 | mStatusBarHeight = getBarHeight(); 262 | } 263 | location += -mActionBarHeight - getMeasuredHeight() - mStatusBarHeight; 264 | mScrollView.smoothScrollBy(0, location); 265 | } 266 | 267 | if (mAssistViewPager != null) { 268 | mIsClick = true; 269 | mAssistViewPager.setCurrentItem(position, true); 270 | } 271 | } 272 | ``` 273 | 这里的点击事件是在 initTabs(tabs) 的时候设置在每个 TabView 上的,这里 TabView 的仅仅是继承了 AppCompatRadioButton 做了一些颜色和背景的设置。点击事件做了两件事,一件是计算 ScrollView 需要滚动的距离并进行平滑滚动,另外一件就是让 ViewPager 进行平滑的滚动。 274 | 275 | 上面在「3」中的 onScrollChange 我们刚才已经知道它对 ViewPager 的滚动进行了判断,当 ViewPager 滚动过程中不会进一步进行处理。但是事实上这里还是会有所影响,因为两者的滚动时间不一致!ScrollView 往往会慢一点,所以常常会发生点击过后 tab 回滚的现象,所以用 mIsClick 进行了进一步判断的处理。 276 | ``` java 277 | 278 | @Override 279 | public void onPageScrollStateChanged(int state) { 280 | if (state == ViewPager.SCROLL_STATE_IDLE) { 281 | TextView tv = getTabView(mSelectedPosition); 282 | if (tv != null) 283 | switch (mIndicatorMode) { 284 | case MATCH_PARENT: 285 | updateIndicator(tv.getLeft(), tv.getMeasuredWidth()); 286 | break; 287 | case WRAP_CONTENT: 288 | int textWidth = getTextWidth(tv); 289 | updateIndicator(tv.getLeft() + tv.getWidth() / 2 - textWidth / 2, textWidth); 290 | break; 291 | } 292 | 293 | /* 294 | * 因 ScrollView 的滚动可能持续比ViewPager长, 295 | * 因此此处不设置延时将存在{@link #onScrollChange(NestedScrollView, int, int, int, int)} 中调用的 isScrolling() 不能拦截掉一些多余的处理, 296 | * 导致indicator回滚的现象, 暂时未考虑到更好的处理方式 297 | */ 298 | if(mIsClick) { 299 | removeCallbacks(mScrollOffRunnable); 300 | postDelayed(mScrollOffRunnable, 200); 301 | }else{ 302 | mScrolling = false; 303 | } 304 | mIsClick = false; 305 | } else { 306 | removeCallbacks(mScrollOffRunnable); 307 | mScrolling = true; 308 | } 309 | 310 | } 311 | 312 | private Runnable mScrollOffRunnable = new Runnable() { 313 | @Override 314 | public void run() { 315 | mScrolling = false; 316 | } 317 | }; 318 | ``` 319 | 可以看到 mIsClick 仅仅是把 mScrolling 延迟 200ms 设置成 false,为了让 ScrollView 先滚完,目前还没想到其他的方法。到这里这个控件可以独立使用了,但是为了实现下面的效果,而不至于监听太乱所以进一步进行优化。 320 | 321 | ![scrollviewindicator2.gif](http://upload-images.jianshu.io/upload_images/5463583-8947587b0a48f24f.gif?imageMogr2/auto-orient/strip) 322 | 323 | 324 | ##### 3.5 ScrollViewTabIndicator 之间的同步 325 | 其实代码很简单,细心的同学可能已经看见了,就是「1」中让大家跳过的 if() 中的语句。 326 | ``` java 327 | if (listener instanceof ScrollViewTabIndicator) { 328 | ScrollViewTabIndicator synchronize = (ScrollViewTabIndicator) listener; 329 | mAssistViewPager = synchronize.getAssistViewPager(); 330 | //接收覆盖监听,避免走多余的监听流程 331 | mScrollListener = synchronize.mScrollListener; 332 | if (mAssistViewPager != null) { 333 | mAssistViewPager.addOnPageChangeListener(this); 334 | } else { 335 | initAssistViewPager(tabs.size()); 336 | } 337 | } else { 338 | initAssistViewPager(tabs.size()); 339 | } 340 | ``` 341 | 判断如果传入的 listener 如果是 ScrollVIewTabIndicator 对象则直接共用创建的辅助动画的 ViewPager,并且接收其中的 mScrollListener。最终会走的 onScrollChange 的只有最后一个控件实现的方法,和最初传进来的 listener。下边看看 5 个控件的同步过程。 342 | ``` java 343 | mTab.setScrollView(mSv,this,names,views); 344 | mTab3.setScrollView(mSv,mTab,names,views); 345 | mTab4.setScrollView(mSv,mTab3,names,views); 346 | mTab5.setScrollView(mSv,mTab4,names,views); 347 | mTab2.setScrollView(mSv,mTab5,names,views); 348 | ``` 349 | 这里只走 this 和 mTab2 的 onScrollChange 方法。 350 | ### 四、重申几个注意点 351 | 352 | * 该类会在 ScrollView 的子 View 中插入一个宽度为 match_parent, 高度为 1px 的 ViewPager (用来辅助动画), 因此确保 ScrollView 中包含的是 ViewGroup; 353 | * 使用时调用 {{@link #setScrollView(NestedScrollView, NestedScrollView.OnScrollChangeListener, List, List)}}与 NestedScrollView 关联,并且原来要设置再 NestedScrollView 上的监听要在此传入, 否则讲被替换; 354 | * TabIndicator 本身也可以作为一个{ NestedScrollView.OnScrollChangeListener} 传入, 如果这样, 两个控件将会同步, 共享已经创建的 ViewPager; 355 | * 默认用48dp的像素值作为 ActionBar 的高度, 计算滚动距离, 如果有需求用{{@link #setActionBarHeight(int)}} 来设置; 356 | 357 | 358 | ### 五、 5月11日更新 359 | ##### 1. 修复快速滑动 tab 没有切换的问题 360 | 不再使用 isScrolling() 方法和 mScrolling 拦截 ScrollView 中的监听,改用 mIsClick 判断;修改原先 mScrollOffRunnable 中的 run 方法。 361 | ~~~java 362 | @Override 363 | public void onPageScrollStateChanged(int state) { 364 | if (state == ViewPager.SCROLL_STATE_IDLE) { 365 | mScrolling = false; 366 | TextView tv = getTabView(mSelectedPosition); 367 | if (tv != null) 368 | switch (mIndicatorMode) { 369 | case MATCH_PARENT: 370 | updateIndicator(tv.getLeft(), tv.getMeasuredWidth()); 371 | break; 372 | case WRAP_CONTENT: 373 | int textWidth = getTextWidth(tv); 374 | updateIndicator(tv.getLeft() + tv.getWidth() / 2 - textWidth / 2, textWidth); 375 | break; 376 | } 377 | 378 | /* 379 | * 因 ScrollView 的滚动可能持续比 ViewPager 长, 380 | * 因此此处不设置延时将存在{@link #onScrollChange(NestedScrollView, int, int, int, int)} 中调用的 mIsClick 不能拦截掉一些多余的处理, 381 | * 导致indicator回滚的现象, 暂时未考虑到更好的处理方式 382 | */ 383 | if (mIsClick) { 384 | removeCallbacks(mScrollOffRunnable); 385 | postDelayed(mScrollOffRunnable, 220); 386 | } 387 | } else { 388 | mScrolling = true; 389 | } 390 | } 391 | 392 | private Runnable mScrollOffRunnable = new Runnable() { 393 | @Override 394 | public void run() { 395 | mIsClick = false; 396 | } 397 | }; 398 | ~~~ 399 | 400 | 修改「3.5」中的同步代码。 401 | ~~~java 402 | if (listener instanceof ScrollViewTabIndicator) { 403 | mSynchronize = (ScrollViewTabIndicator) listener; 404 | mSynchronize.mNextSynchronize = this; 405 | mAssistViewPager = mSynchronize.getAssistViewPager(); 406 | //接收覆盖监听,避免走多余的监听流程 407 | mScrollListener = mSynchronize.mScrollListener; 408 | if (mAssistViewPager != null) { 409 | mAssistViewPager.addOnPageChangeListener(this); 410 | } else { 411 | initAssistViewPager(tabs.size()); 412 | } 413 | } else { 414 | initAssistViewPager(tabs.size()); 415 | } 416 | ~~~ 417 | 可以看到这里不仅保持传进来的 ScrollViewTabIndicator 对象为 mSynchronize,而且如果本身如果被设置给其他的 ScrollViewTabIndicator 他的 mNextSynchronize 也会被赋值。保持这两个引用主要是为了保证多个 ScrollViewTabIndicator 同步时,在点击不同对象的 tab 的时候,他们的 mIsClick 能保持一致,下边看一下 onClick(View v) 方法的改变。 418 | ~~~java 419 | @Override 420 | public void onClick(android.view.View v) { 421 | 422 | int position = (Integer) v.getTag(); 423 | 424 | if (mAssistViewPager != null) { 425 | synchronizeClickStatus(); 426 | mAssistViewPager.setCurrentItem(position, true); 427 | } 428 | 429 | if (mScrollView != null) { 430 | int location; 431 | location = getViewLocation(position); 432 | // 待滑动距离 = 当前坐标 - (ActionBar高度) - indicator高度 - 状态栏高度 433 | if (mStatusBarHeight == 0) { 434 | mStatusBarHeight = getBarHeight(); 435 | } 436 | location += -mActionBarHeight - getMeasuredHeight() - mStatusBarHeight; 437 | 438 | // location -= getViewMarginTop(position); 439 | //因为这里经常会出现 scrollView 没有滚动的现象这里才加了 delay 440 | final int finalLocation = position == 0 ? (location > 0 ? location - 1 : location + 1) : location; 441 | mScrollView.postDelayed(new Runnable() { 442 | @Override 443 | public void run() { 444 | mScrollView.smoothScrollBy(0, finalLocation); 445 | } 446 | }, 100); 447 | } 448 | 449 | } 450 | 451 | private void synchronizeClickStatus() { 452 | mIsClick = true; 453 | if (mSynchronize != null && !mSynchronize.mIsClick) { 454 | mSynchronize.synchronizeClickStatus(); 455 | } 456 | if (mNextSynchronize != null && !mNextSynchronize.mIsClick) { 457 | mNextSynchronize.synchronizeClickStatus(); 458 | } 459 | } 460 | 461 | ~~~ 462 | 修改了两方面,一是点击的时候同时修改了前一个和后一个同步的 ScrollViewTabIndicator 的 mIsClick,二是因为直接调用 ScrollView 的 smoothScrollBy 方法,如果点击速度过快 smoothScrollBy 方法中对点击的时间间隔做了判断,导致 ScrollView 常常滚动不到预期的位置,所以做了 100 毫秒的延迟处理。 463 | 464 | 到这里对于快速滚动的处理算是完成了,可能还有其他的问题,如果大家有发现问题或者意见还望提醒我改正,谢谢。 465 | 466 | 467 | ### 六、 最后 468 | 感谢 [J!nL!n](https://daijinlin.com/) 同学的 TabIndicator 以及 CF 同学的思路。 469 | 470 | [这里有源码](https://github.com/DthFish/ScrollViewIndicator) 471 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | .DS_Store 3 | *.iml 4 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | defaultConfig { 7 | applicationId "com.dthfish.scrollviewindicator" 8 | minSdkVersion 15 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | 24 | compile 'com.android.support:appcompat-v7:25.2.0' 25 | 26 | } 27 | -------------------------------------------------------------------------------- /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:\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 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/dthfish/scrollviewindicator/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.dthfish.scrollviewindicator; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v4.widget.NestedScrollView; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.view.View; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class MainActivity extends AppCompatActivity implements NestedScrollView.OnScrollChangeListener{ 13 | 14 | private NestedScrollView mSv; 15 | private ScrollViewTabIndicator mTab; 16 | private ScrollViewTabIndicator mTab2; 17 | private int[] mTabMiddleLocation = new int[2]; 18 | private int[] mTabTopLocation = new int[2]; 19 | 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_main); 25 | initView(); 26 | } 27 | 28 | private void initView() { 29 | mSv = (NestedScrollView) findViewById(R.id.sv); 30 | mTab = (ScrollViewTabIndicator) findViewById(R.id.tab); 31 | mTab2 = (ScrollViewTabIndicator) findViewById(R.id.tab2); 32 | 33 | View view1 = findViewById(R.id.tv_1); 34 | View view2 = findViewById(R.id.tv_2); 35 | View view3 = findViewById(R.id.tv_3); 36 | View view4 = findViewById(R.id.tv_4); 37 | List names = new ArrayList<>(); 38 | names.add("详情"); 39 | names.add("评论"); 40 | names.add("须知"); 41 | names.add("更多"); 42 | List views = new ArrayList<>(); 43 | views.add(view1); 44 | views.add(view2); 45 | views.add(view3); 46 | views.add(view4); 47 | mTab.setScrollView(mSv,this,names,views); 48 | //将mTab本身作为参数传入mTab2已达到同步状态 49 | mTab2.setScrollView(mSv,mTab,names,views); 50 | 51 | findViewById(R.id.tv_banner).setOnClickListener(new View.OnClickListener() { 52 | @Override 53 | public void onClick(View v) { 54 | startActivity(new Intent(MainActivity.this,SecondActivity.class)); 55 | } 56 | }); 57 | } 58 | 59 | @Override 60 | public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { 61 | setVisibleAndGone(); 62 | } 63 | 64 | private void setVisibleAndGone() { 65 | mTab2.getLocationOnScreen(mTabMiddleLocation); 66 | mTab.getLocationOnScreen(mTabTopLocation); 67 | if (mTabMiddleLocation[1] <= mTabTopLocation[1]) { 68 | mTab.setVisibility(View.VISIBLE); 69 | mTab2.setVisibility(View.INVISIBLE); 70 | } else { 71 | mTab.setVisibility(View.INVISIBLE); 72 | mTab2.setVisibility(View.VISIBLE); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/dthfish/scrollviewindicator/ScrollViewTabIndicator.java: -------------------------------------------------------------------------------- 1 | package com.dthfish.scrollviewindicator; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.res.Resources; 6 | import android.content.res.TypedArray; 7 | import android.graphics.Canvas; 8 | import android.graphics.Color; 9 | import android.graphics.Paint; 10 | import android.graphics.drawable.ColorDrawable; 11 | import android.graphics.drawable.Drawable; 12 | import android.os.Build; 13 | import android.os.Build.VERSION_CODES; 14 | import android.support.annotation.NonNull; 15 | import android.support.v4.view.PagerAdapter; 16 | import android.support.v4.view.ViewPager; 17 | import android.support.v4.widget.NestedScrollView; 18 | import android.support.v4.widget.Space; 19 | import android.text.TextUtils; 20 | import android.util.AttributeSet; 21 | import android.util.TypedValue; 22 | import android.view.Gravity; 23 | import android.view.View; 24 | import android.view.View.OnClickListener; 25 | import android.view.ViewGroup; 26 | import android.widget.LinearLayout; 27 | import android.widget.TextView; 28 | 29 | import java.lang.reflect.Field; 30 | import java.util.ArrayList; 31 | import java.util.Collections; 32 | import java.util.List; 33 | 34 | /** 35 | * Created by J!nl!n on 2016/7/12 17:08. 36 | * Copyright © 1990-2015 J!nl!n™ Inc. All rights reserved. 37 | *

38 | * ━━━━━━神兽出没━━━━━━ 39 | *    ┏┓   ┏┓ 40 | *   ┏┛┻━━━┛┻┓ 41 | *   ┃       ┃ 42 | *   ┃   ━   ┃ 43 | *   ┃ ┳┛ ┗┳ ┃ 44 | *   ┃       ┃ 45 | *   ┃   ┻   ┃ 46 | *   ┃       ┃ 47 | *   ┗━┓   ┏━┛Code is far away from bug with the animal protecting 48 | *     ┃   ┃ 神兽保佑,代码无bug 49 | *     ┃   ┃ 50 | *     ┃   ┗━━━┓ 51 | *     ┃       ┣┓ 52 | *     ┃       ┏┛ 53 | *     ┗┓┓┏━┳┓┏┛ 54 | *      ┃┫┫ ┃┫┫ 55 | *      ┗┻┛ ┗┻┛ 56 | * ━━━━━━感觉萌萌哒━━━━━━ 57 | *

58 | * modified by zlz on 2017/3/16 59 | * 为 NestedScrollView 改造的 ViewPager TabIndicator, 60 | * 请注意: 61 | * 1、该类会在 ScrollView 的子 View 中插入一个宽度为 match_parent, 高度为 1px 的 ViewPager (用来辅助动画), 因此确保 ScrollView 中包含的是 ViewGroup 62 | * 2、使用时调用 {{@link #setScrollView(NestedScrollView, NestedScrollView.OnScrollChangeListener, List, List)}}与 NestedScrollView 关联, 63 | * 并且原来要设置再 NestedScrollView 上的监听要在此传入, 否则讲被替换 64 | * 3、TabIndicator 本身也可以作为一个{ NestedScrollView.OnScrollChangeListener} 传入, 如果这样, 两个控件将会同步, 共享已经创建的 ViewPager 65 | * 4、默认用48dp的像素值作为 ActionBar 的高度, 计算滚动距离, 如果有需求用{{@link #setActionBarHeight(int)}} 来设置 66 | */ 67 | public class ScrollViewTabIndicator extends LinearLayout implements ViewPager.OnPageChangeListener, OnClickListener, NestedScrollView.OnScrollChangeListener { 68 | 69 | private int mMode; 70 | private int mTabPadding; 71 | private int mTextAppearance; 72 | private int tabBackground; 73 | 74 | private int mIndicatorOffset; 75 | private int mIndicatorWidth; 76 | private int mIndicatorHeight; 77 | private int mIndicatorMode; 78 | 79 | private int mUnderLineHeight; 80 | 81 | private Paint mPaint; 82 | private Paint mUnderLinePaint; 83 | 84 | public static final int MODE_SCROLL = 0; 85 | public static final int MODE_FIXED = 1; 86 | 87 | private int mSelectedPosition; 88 | private boolean mScrolling = false; 89 | 90 | private Runnable mTabAnimSelector; 91 | 92 | private static final int MATCH_PARENT = -1; 93 | private static final int WRAP_CONTENT = -2; 94 | private int mActionBarHeight = dip2px(48); 95 | private boolean mIsClick = false; 96 | /** 97 | * 仅仅用来协助完成动画 98 | */ 99 | private ViewPager mAssistViewPager; 100 | 101 | private NestedScrollView mScrollView; 102 | private NestedScrollView.OnScrollChangeListener mScrollListener; 103 | private List mViews; 104 | 105 | public ScrollViewTabIndicator(Context context) { 106 | super(context); 107 | 108 | init(context, null, 0, 0); 109 | } 110 | 111 | public ScrollViewTabIndicator(Context context, AttributeSet attrs) { 112 | super(context, attrs); 113 | 114 | init(context, attrs, 0, 0); 115 | } 116 | 117 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 118 | public ScrollViewTabIndicator(Context context, AttributeSet attrs, int defStyleAttr) { 119 | super(context, attrs, defStyleAttr); 120 | 121 | init(context, attrs, defStyleAttr, 0); 122 | } 123 | 124 | @TargetApi(VERSION_CODES.LOLLIPOP) 125 | public ScrollViewTabIndicator(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 126 | super(context, attrs, defStyleAttr); 127 | 128 | init(context, attrs, defStyleAttr, defStyleRes); 129 | } 130 | 131 | private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 132 | setGravity(Gravity.CENTER_VERTICAL); 133 | setHorizontalScrollBarEnabled(false); 134 | 135 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 136 | mPaint.setStyle(Paint.Style.FILL); 137 | 138 | mUnderLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 139 | mUnderLinePaint.setStyle(Paint.Style.FILL); 140 | 141 | applyStyle(context, attrs, defStyleAttr, defStyleRes); 142 | 143 | if (isInEditMode()) 144 | addTemporaryTab(); 145 | } 146 | 147 | @SuppressWarnings("unused") 148 | public void applyStyle(int resId) { 149 | applyStyle(getContext(), null, 0, resId); 150 | } 151 | 152 | private void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 153 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ScrollViewTabIndicator, defStyleAttr, defStyleRes); 154 | int indicatorColor; 155 | int underLineColor; 156 | try { 157 | mTabPadding = a.getDimensionPixelSize(R.styleable.ScrollViewTabIndicator_tpi_tabPadding, 12); 158 | indicatorColor = a.getColor(R.styleable.ScrollViewTabIndicator_tpi_indicatorColor, Color.WHITE); 159 | mIndicatorMode = a.getInt(R.styleable.ScrollViewTabIndicator_tpi_indicatorMode, MATCH_PARENT); /* MATCH_PARENT = -1 && WRAP_CONTENT = -2 */ 160 | mIndicatorHeight = a.getDimensionPixelSize(R.styleable.ScrollViewTabIndicator_tpi_indicatorHeight, 2); 161 | underLineColor = a.getColor(R.styleable.ScrollViewTabIndicator_tpi_underLineColor, Color.LTGRAY); 162 | mUnderLineHeight = a.getDimensionPixelSize(R.styleable.ScrollViewTabIndicator_tpi_underLineHeight, 1); 163 | mTextAppearance = a.getResourceId(R.styleable.ScrollViewTabIndicator_android_textAppearance, 0); 164 | tabBackground = a.getResourceId(R.styleable.ScrollViewTabIndicator_tpi_tabBackground, 0); 165 | mMode = a.getInteger(R.styleable.ScrollViewTabIndicator_tpi_mode, MODE_SCROLL); 166 | } finally { 167 | a.recycle(); 168 | } 169 | removeAllViews(); 170 | 171 | mPaint.setColor(indicatorColor); 172 | mUnderLinePaint.setColor(underLineColor); 173 | } 174 | 175 | @Override 176 | public void onAttachedToWindow() { 177 | super.onAttachedToWindow(); 178 | // Re-post the selector we saved 179 | if (mTabAnimSelector != null) 180 | post(mTabAnimSelector); 181 | } 182 | 183 | public void setActionBarHeight(int height) { 184 | mActionBarHeight = height; 185 | } 186 | 187 | @Override 188 | public void onDetachedFromWindow() { 189 | super.onDetachedFromWindow(); 190 | removeCallbacks(mScrollOffRunnable); 191 | if (mTabAnimSelector != null) 192 | removeCallbacks(mTabAnimSelector); 193 | } 194 | 195 | private TabView getTabView(int position) { 196 | 197 | return (TabView) getChildAt(position); 198 | } 199 | 200 | private void animateToTab(final int position) { 201 | final TabView tv = getTabView(position); 202 | if (tv == null) 203 | return; 204 | 205 | if (mTabAnimSelector != null) 206 | removeCallbacks(mTabAnimSelector); 207 | 208 | mTabAnimSelector = new Runnable() { 209 | public void run() { 210 | if (!mScrolling) 211 | switch (mIndicatorMode) { 212 | case MATCH_PARENT: 213 | updateIndicator(tv.getLeft(), tv.getWidth()); 214 | break; 215 | case WRAP_CONTENT: 216 | int textWidth = getTextWidth(tv); 217 | updateIndicator(tv.getLeft() + tv.getWidth() / 2 - textWidth / 2, textWidth); 218 | break; 219 | } 220 | mTabAnimSelector = null; 221 | } 222 | }; 223 | 224 | post(mTabAnimSelector); 225 | } 226 | 227 | 228 | public void updateIndicator(int offset, int width) { 229 | mIndicatorOffset = offset; 230 | mIndicatorWidth = width; 231 | invalidate(); 232 | } 233 | 234 | @Override 235 | public void draw(@NonNull Canvas canvas) { 236 | super.draw(canvas); 237 | 238 | // draw underline 239 | canvas.drawRect(0, getHeight() - mUnderLineHeight, getWidth(), getHeight(), mUnderLinePaint); // must do it first 240 | 241 | int x = mIndicatorOffset + getPaddingLeft(); 242 | canvas.drawRect(x, getHeight() - mIndicatorHeight, x + mIndicatorWidth, getHeight(), mPaint); 243 | 244 | if (isInEditMode()) 245 | canvas.drawRect(getPaddingLeft(), getHeight() - mIndicatorHeight, getPaddingLeft() + getChildAt(0).getWidth(), getHeight(), mPaint); 246 | } 247 | 248 | @Override 249 | public void onPageScrollStateChanged(int state) { 250 | if (state == ViewPager.SCROLL_STATE_IDLE) { 251 | mScrolling = false; 252 | TextView tv = getTabView(mSelectedPosition); 253 | if (tv != null) 254 | switch (mIndicatorMode) { 255 | case MATCH_PARENT: 256 | updateIndicator(tv.getLeft(), tv.getMeasuredWidth()); 257 | break; 258 | case WRAP_CONTENT: 259 | int textWidth = getTextWidth(tv); 260 | updateIndicator(tv.getLeft() + tv.getWidth() / 2 - textWidth / 2, textWidth); 261 | break; 262 | } 263 | 264 | /* 265 | * 因 ScrollView 的滚动可能持续比ViewPager长, 266 | * 因此此处不设置延时将存在{@link #onScrollChange(NestedScrollView, int, int, int, int)} 中调用的 mIsClick 不能拦截掉一些多余的处理, 267 | * 导致indicator回滚的现象, 暂时未考虑到更好的处理方式 268 | */ 269 | if (mIsClick) { 270 | removeCallbacks(mScrollOffRunnable); 271 | postDelayed(mScrollOffRunnable, 220); 272 | } 273 | } else { 274 | mScrolling = true; 275 | } 276 | } 277 | 278 | @Override 279 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 280 | TabView tv_scroll = getTabView(position); 281 | TabView tv_next = getTabView(position + 1); 282 | 283 | if (tv_scroll != null && tv_next != null) { 284 | int width_scroll = mIndicatorMode == MATCH_PARENT ? tv_scroll.getWidth() : getTextWidth(tv_scroll); 285 | int width_next = mIndicatorMode == MATCH_PARENT ? tv_next.getWidth() : getTextWidth(tv_next); 286 | float distance = mIndicatorMode == MATCH_PARENT ? (width_scroll + width_next) / 2f : width_scroll / 2 + tv_scroll.getWidth() / 2 + tv_next.getWidth() / 2 - width_next / 2; 287 | 288 | int width = (int) (width_scroll + (width_next - width_scroll) * positionOffset + 0.5f); 289 | int offset = mIndicatorMode == MATCH_PARENT ? 290 | (int) (tv_scroll.getLeft() + width_scroll / 2f + distance * positionOffset - width / 2f + 0.5f) 291 | : (int) (tv_scroll.getLeft() + tv_scroll.getWidth() / 2 - width_scroll / 2 + distance * positionOffset + 0.5f); 292 | updateIndicator(offset, width); 293 | } 294 | } 295 | 296 | @Override 297 | public void onPageSelected(int position) { 298 | setCurrentItem(position); 299 | } 300 | 301 | private Runnable mScrollOffRunnable = new Runnable() { 302 | @Override 303 | public void run() { 304 | mIsClick = false; 305 | } 306 | }; 307 | 308 | public int getTextWidth(TextView tv) { 309 | return (int) tv.getPaint().measureText(tv.getText().toString()); 310 | } 311 | 312 | private int mStatusBarHeight; 313 | 314 | @Override 315 | public void onClick(android.view.View v) { 316 | 317 | int position = (Integer) v.getTag(); 318 | 319 | if (mAssistViewPager != null) { 320 | synchronizeClickStatus(); 321 | mAssistViewPager.setCurrentItem(position, true); 322 | } 323 | 324 | if (mScrollView != null) { 325 | int location; 326 | location = getViewLocation(position); 327 | // 待滑动距离 = 当前坐标 - (ActionBar高度) - indicator高度 - 状态栏高度 328 | 329 | location += -mActionBarHeight - getMeasuredHeight() - mStatusBarHeight; 330 | 331 | // location -= getViewMarginTop(position); 332 | //因为这里经常会出现 scrollView 没有滚动的现象这里才加了 delay 333 | final int finalLocation = position == 0 ? (location > 0 ? location - 1 : location + 1) : location; 334 | mScrollView.postDelayed(new Runnable() { 335 | @Override 336 | public void run() { 337 | mScrollView.smoothScrollBy(0, finalLocation); 338 | } 339 | }, 100); 340 | } 341 | 342 | } 343 | 344 | @Override 345 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 346 | super.onLayout(changed, l, t, r, b); 347 | if (mStatusBarHeight == 0) { 348 | mStatusBarHeight = getBarHeight(); 349 | } 350 | } 351 | 352 | private void synchronizeClickStatus() { 353 | mIsClick = true; 354 | if (mSynchronize != null && !mSynchronize.mIsClick) { 355 | mSynchronize.synchronizeClickStatus(); 356 | } 357 | if (mNextSynchronize != null && !mNextSynchronize.mIsClick) { 358 | mNextSynchronize.synchronizeClickStatus(); 359 | } 360 | } 361 | 362 | private int getViewMarginTop(int position) { 363 | int marginTop = 0; 364 | if (mViews != null && mViews.size() > position) { 365 | View view = mViews.get(position); 366 | if (view != null) { 367 | try { 368 | ViewGroup.MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams(); 369 | marginTop = params.topMargin; 370 | } catch (ClassCastException e) { 371 | e.printStackTrace(); 372 | } 373 | } 374 | } 375 | return marginTop; 376 | } 377 | 378 | private int getViewLocation(int position) { 379 | if (mViews != null && mViews.size() > position) { 380 | View view = mViews.get(position); 381 | if (view != null) { 382 | int[] location = new int[2]; 383 | view.getLocationOnScreen(location); 384 | return location[1]; 385 | } 386 | } 387 | return 0; 388 | } 389 | 390 | /** 391 | * Set the current page of this TabPageIndicator. 392 | * 393 | * @param position The position of current page. 394 | */ 395 | public void setCurrentItem(int position) { 396 | if (mSelectedPosition != position) { 397 | TabView tv = getTabView(mSelectedPosition); 398 | if (tv != null) 399 | tv.setChecked(false); 400 | } 401 | 402 | mSelectedPosition = position; 403 | TabView tv = getTabView(mSelectedPosition); 404 | if (tv != null) 405 | tv.setChecked(true); 406 | animateToTab(position); 407 | } 408 | 409 | public int getCurrentIndex() { 410 | return mSelectedPosition; 411 | } 412 | 413 | public static int dip2px(int dpValue) { 414 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, Resources.getSystem().getDisplayMetrics()); 415 | } 416 | 417 | /** 418 | * 获取当前状态栏 419 | */ 420 | public int getBarHeight() { 421 | Class c = null; 422 | Object obj = null; 423 | Field field = null; 424 | int x = 0, sbar = 38;//默认为38,貌似大部分是这样的 425 | 426 | try { 427 | c = Class.forName("com.android.internal.R$dimen"); 428 | obj = c.newInstance(); 429 | field = c.getField("status_bar_height"); 430 | x = Integer.parseInt(field.get(obj).toString()); 431 | sbar = getResources().getDimensionPixelSize(x); 432 | 433 | } catch (Exception e1) { 434 | e1.printStackTrace(); 435 | } 436 | return sbar; 437 | } 438 | 439 | 440 | @Override 441 | public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { 442 | 443 | if (mScrollListener != null) { 444 | mScrollListener.onScrollChange(v, scrollX, scrollY, oldScrollX, oldScrollY); 445 | } 446 | 447 | if (mIsClick) { 448 | return; 449 | } 450 | 451 | int top = mActionBarHeight + getMeasuredHeight() + mStatusBarHeight;//TitleBar 高度 + 控件高度 + StatusBar 高度 452 | List locations = new ArrayList<>(); 453 | for (int i = 0, size = mViews.size(); i < size; i++) { 454 | locations.add(getViewLocation(i)); 455 | } 456 | Collections.sort(locations); 457 | int position = 0; 458 | 459 | if (top < locations.get(0)) { 460 | position = 0; 461 | } else { 462 | for (int j = 0, size = locations.size(); j < size; j++) { 463 | if (j + 1 == size) { 464 | position = j; 465 | break; 466 | } 467 | if (top >= locations.get(j) && top < locations.get(j + 1)) { 468 | //如果已经不能向下滚动了就 469 | if (!v.canScrollVertically(VERTICAL)) { 470 | position = size - 1; 471 | break; 472 | } 473 | 474 | position = j; 475 | break; 476 | } 477 | } 478 | } 479 | if (getCurrentIndex() == position) { 480 | return; 481 | } 482 | mAssistViewPager.setCurrentItem(position, true); 483 | } 484 | 485 | /** 486 | * 因为会替换 scrollview 上的 listener, 所以要传进来. 487 | * 如果传进来是一个 TabIndicator 对象, 则两者状态会同步, 并且自定义的滚动监听要设置到第一个上边 488 | * 489 | * @param scrollView 监听的 NestedScrollView 490 | * @param listener 原先设置在 NestedScrollView 上的监听 491 | * @param tabs tab 的标题 492 | * @param views 各个 tab 对应的需要滚动到的 View 493 | */ 494 | public void setScrollView(NestedScrollView scrollView, NestedScrollView.OnScrollChangeListener listener, List tabs, List views) { 495 | if (mScrollView == scrollView) { 496 | return; 497 | } 498 | 499 | if (tabs == null || views == null) { 500 | throw new IllegalArgumentException("tabs and views should not be null!"); 501 | } 502 | if (tabs.isEmpty() || views.isEmpty()) { 503 | throw new IllegalArgumentException("tabs and views should not be empty!"); 504 | } 505 | if (tabs.size() != views.size()) { 506 | throw new IllegalArgumentException("tabs and views should be the same length!"); 507 | } 508 | mScrollListener = listener; 509 | mScrollView = scrollView; 510 | mViews = views; 511 | if (mScrollView != null) { 512 | mScrollView.setOnScrollChangeListener(this); 513 | } 514 | initTabs(tabs); 515 | if (listener instanceof ScrollViewTabIndicator) { 516 | mSynchronize = (ScrollViewTabIndicator) listener; 517 | mSynchronize.mNextSynchronize = this; 518 | mAssistViewPager = mSynchronize.getAssistViewPager(); 519 | //接收覆盖监听,避免走多余的监听流程 520 | mScrollListener = mSynchronize.mScrollListener; 521 | 522 | if (mAssistViewPager != null) { 523 | mAssistViewPager.addOnPageChangeListener(this); 524 | } else { 525 | initAssistViewPager(tabs.size()); 526 | } 527 | } else { 528 | initAssistViewPager(tabs.size()); 529 | } 530 | } 531 | 532 | //传入 533 | private ScrollViewTabIndicator mSynchronize; 534 | 535 | //仅仅用于设置mIsClick 536 | private ScrollViewTabIndicator mNextSynchronize; 537 | 538 | private ViewPager getAssistViewPager() { 539 | 540 | return mAssistViewPager; 541 | } 542 | 543 | private void initAssistViewPager(int size) { 544 | if (mAssistViewPager != null) { 545 | return; 546 | } 547 | mAssistViewPager = new ViewPager(getContext()); 548 | ViewGroup.LayoutParams p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1); 549 | mAssistViewPager.setLayoutParams(p); 550 | 551 | //请注意这段代码, 因为会在 ScrollView 的子 view 中插入一个 ViewPager 552 | View viewGroup = mScrollView.getChildAt(0); 553 | if (viewGroup == null) { 554 | throw new IllegalStateException(" The child view of the ScrollView must be not null!"); 555 | } 556 | if (!(viewGroup instanceof ViewGroup)) { 557 | throw new IllegalStateException(" The child view of the ScrollView must be a ViewGroup!"); 558 | } 559 | viewGroup = mScrollView.getChildAt(0); 560 | ((ViewGroup) viewGroup).addView(mAssistViewPager); 561 | 562 | 563 | final List viewList = new ArrayList<>(); 564 | for (int i = 0; i < size; i++) { 565 | viewList.add(new Space(getContext())); 566 | } 567 | mAssistViewPager.setAdapter(new PagerAdapter() { 568 | @Override 569 | public int getCount() { 570 | return viewList.size(); 571 | } 572 | 573 | @Override 574 | public boolean isViewFromObject(View view, Object object) { 575 | return view == object; 576 | } 577 | 578 | @Override 579 | public Object instantiateItem(ViewGroup container, int position) { 580 | container.addView(viewList.get(position)); 581 | return viewList.get(position); 582 | } 583 | 584 | @Override 585 | public void destroyItem(ViewGroup container, int position, Object object) { 586 | container.removeView(viewList.get(position)); 587 | } 588 | }); 589 | mAssistViewPager.addOnPageChangeListener(this); 590 | } 591 | 592 | private void initTabs(List tabs) { 593 | removeAllViews(); 594 | final int count = tabs.size(); 595 | 596 | if (mSelectedPosition > count) 597 | mSelectedPosition = count - 1; 598 | 599 | for (int i = 0; i < count; i++) { 600 | String title = tabs.get(i); 601 | if (TextUtils.isEmpty(title)) 602 | title = "NULL"; 603 | 604 | TabView tv = new TabView(getContext()); 605 | tv.setText(title); 606 | tv.setTag(i); 607 | 608 | if (mMode == MODE_SCROLL) { 609 | LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 610 | lp.leftMargin = mTabPadding; 611 | lp.rightMargin = mTabPadding; 612 | addView(tv, lp); 613 | } else if (mMode == MODE_FIXED) { 614 | LayoutParams params = new LayoutParams(0, LayoutParams.MATCH_PARENT); 615 | params.weight = 1f; 616 | addView(tv, params); 617 | } 618 | } 619 | setCurrentItem(mSelectedPosition); 620 | requestLayout(); 621 | } 622 | 623 | private void addTemporaryTab() { 624 | for (int i = 0; i < 3; i++) { 625 | CharSequence title = null; 626 | if (i == 0) 627 | title = "流行新品"; 628 | else if (i == 1) 629 | title = "最近上新"; 630 | else if (i == 2) 631 | title = "人气热销"; 632 | 633 | TabView tv = new TabView(getContext()); 634 | tv.setText(title); 635 | tv.setTag(i); 636 | tv.setChecked(i == 0); 637 | if (mMode == MODE_SCROLL) { 638 | tv.setPadding(mTabPadding, 0, mTabPadding, 0); 639 | addView(tv, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); 640 | } else if (mMode == MODE_FIXED) { 641 | LayoutParams params = new LayoutParams(0, LayoutParams.MATCH_PARENT); 642 | params.weight = 1f; 643 | addView(tv, params); 644 | } 645 | } 646 | } 647 | 648 | 649 | class TabView extends android.support.v7.widget.AppCompatRadioButton { 650 | public TabView(Context context) { 651 | super(context, null, mTextAppearance); 652 | init(); 653 | } 654 | 655 | private void init() { 656 | setButtonDrawable(new ColorDrawable(Color.TRANSPARENT)); 657 | setGravity(Gravity.CENTER); 658 | setTextAppearance(getContext(), mTextAppearance); 659 | if (0 != tabBackground) { 660 | setBackgroundResource(tabBackground); 661 | } else { 662 | setBackground(this, new ColorDrawable(Color.TRANSPARENT)); 663 | } 664 | setSingleLine(true); 665 | setEllipsize(TextUtils.TruncateAt.END); 666 | setOnClickListener(ScrollViewTabIndicator.this); 667 | } 668 | 669 | public void setBackground(View view, Drawable drawable) { 670 | if (view == null) { 671 | return; 672 | } 673 | int left = view.getPaddingLeft(); 674 | int right = view.getPaddingRight(); 675 | int top = view.getPaddingTop(); 676 | int bottom = view.getPaddingBottom(); 677 | if (Build.VERSION.SDK_INT >= 16) { 678 | view.setBackground(drawable); 679 | } else { 680 | view.setBackgroundDrawable(drawable); 681 | } 682 | view.setPadding(left, top, right, bottom); 683 | } 684 | } 685 | 686 | 687 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dthfish/scrollviewindicator/SecondActivity.java: -------------------------------------------------------------------------------- 1 | package com.dthfish.scrollviewindicator; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.widget.NestedScrollView; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.view.View; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | /** 13 | * Description ${Desc} 14 | * Author zlz 15 | * Date 2017/3/31. 16 | */ 17 | 18 | public class SecondActivity extends AppCompatActivity implements NestedScrollView.OnScrollChangeListener { 19 | 20 | private NestedScrollView mSv; 21 | private ScrollViewTabIndicator mTab; 22 | private ScrollViewTabIndicator mTab2; 23 | 24 | private ScrollViewTabIndicator mTab3; 25 | private ScrollViewTabIndicator mTab4; 26 | private ScrollViewTabIndicator mTab5; 27 | 28 | @Override 29 | protected void onCreate(@Nullable Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.activity_second); 32 | 33 | mSv = (NestedScrollView) findViewById(R.id.sv); 34 | mTab = (ScrollViewTabIndicator) findViewById(R.id.tab); 35 | mTab2 = (ScrollViewTabIndicator) findViewById(R.id.tab2); 36 | mTab3 = (ScrollViewTabIndicator) findViewById(R.id.tab3); 37 | mTab4 = (ScrollViewTabIndicator) findViewById(R.id.tab4); 38 | mTab5 = (ScrollViewTabIndicator) findViewById(R.id.tab5); 39 | View view1 = findViewById(R.id.tv_1); 40 | View view2 = findViewById(R.id.tv_2); 41 | View view3 = findViewById(R.id.tv_3); 42 | List names = new ArrayList<>(); 43 | names.add("详情"); 44 | names.add("评论"); 45 | names.add("须知"); 46 | List views = new ArrayList<>(); 47 | views.add(view1); 48 | views.add(view2); 49 | views.add(view3); 50 | mTab.setScrollView(mSv,this,names,views); 51 | 52 | mTab3.setScrollView(mSv,mTab,names,views); 53 | mTab4.setScrollView(mSv,mTab3,names,views); 54 | mTab5.setScrollView(mSv,mTab4,names,views); 55 | mTab2.setScrollView(mSv,mTab5,names,views); 56 | } 57 | 58 | @Override 59 | public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_666666_to_d30775_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 17 | 18 | 23 | 24 | 28 | 35 | 36 | 47 | 48 | 54 | 55 | 62 | 69 | 70 | 77 | 78 | 79 | 80 | 81 | 94 | 95 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_second.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 16 | 17 | 22 | 23 | 27 | 34 | 35 | 46 | 47 | 53 | 54 | 61 | 68 | 69 | 70 | 71 | 72 | 85 | 97 | 109 | 121 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DthFish/ScrollViewIndicator/5e6c8c0a5312de074e8885b6755e272ffe5b85ef/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DthFish/ScrollViewIndicator/5e6c8c0a5312de074e8885b6755e272ffe5b85ef/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DthFish/ScrollViewIndicator/5e6c8c0a5312de074e8885b6755e272ffe5b85ef/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DthFish/ScrollViewIndicator/5e6c8c0a5312de074e8885b6755e272ffe5b85ef/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DthFish/ScrollViewIndicator/5e6c8c0a5312de074e8885b6755e272ffe5b85ef/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DthFish/ScrollViewIndicator/5e6c8c0a5312de074e8885b6755e272ffe5b85ef/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DthFish/ScrollViewIndicator/5e6c8c0a5312de074e8885b6755e272ffe5b85ef/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DthFish/ScrollViewIndicator/5e6c8c0a5312de074e8885b6755e272ffe5b85ef/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DthFish/ScrollViewIndicator/5e6c8c0a5312de074e8885b6755e272ffe5b85ef/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DthFish/ScrollViewIndicator/5e6c8c0a5312de074e8885b6755e272ffe5b85ef/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ScrollViewIndicator 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.3.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DthFish/ScrollViewIndicator/5e6c8c0a5312de074e8885b6755e272ffe5b85ef/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Mar 16 11:52:25 CST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------