├── .gitignore
├── .idea
├── compiler.xml
├── copyright
│ └── profiles_settings.xml
├── gradle.xml
├── markdown-navigator.xml
├── markdown-navigator
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
├── runConfigurations.xml
└── vcs.xml
├── CHANGELOG.md
├── License.txt
├── README.md
├── assets
├── bubble.gif
├── flip.gif
├── pop.gif
└── simple.gif
├── build.gradle
├── demo
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── tompee
│ │ └── funtablayoutsample
│ │ ├── MainActivity.java
│ │ ├── bubbletablayout
│ │ └── BubbleTabLayoutActivity.java
│ │ ├── fliptablayout
│ │ └── FlipTabLayoutActivity.java
│ │ ├── fragment
│ │ └── SampleFragment.java
│ │ ├── poptablayout
│ │ └── PopTabLayoutActivity.java
│ │ └── simpletablayout
│ │ └── SimpleTabLayoutActivity.java
│ └── res
│ ├── drawable-hdpi
│ ├── ic_call_white_48dp.png
│ ├── ic_chat_white_48dp.png
│ ├── ic_create_white_48dp.png
│ ├── ic_games_white_48dp.png
│ └── ic_mail_white_48dp.png
│ ├── drawable-mdpi
│ ├── ic_call_white_48dp.png
│ ├── ic_chat_white_48dp.png
│ ├── ic_create_white_48dp.png
│ ├── ic_games_white_48dp.png
│ └── ic_mail_white_48dp.png
│ ├── drawable-v21
│ └── ripple.xml
│ ├── drawable-xhdpi
│ ├── ic_call_white_48dp.png
│ ├── ic_chat_white_48dp.png
│ ├── ic_create_white_48dp.png
│ ├── ic_games_white_48dp.png
│ └── ic_mail_white_48dp.png
│ ├── drawable-xxhdpi
│ ├── ic_call_white_48dp.png
│ ├── ic_chat_white_48dp.png
│ ├── ic_create_white_48dp.png
│ ├── ic_games_white_48dp.png
│ └── ic_mail_white_48dp.png
│ ├── drawable-xxxhdpi
│ ├── ic_call_white_48dp.png
│ ├── ic_chat_white_48dp.png
│ ├── ic_create_white_48dp.png
│ ├── ic_games_white_48dp.png
│ └── ic_mail_white_48dp.png
│ ├── drawable
│ └── ripple.xml
│ ├── layout
│ ├── activity_main.xml
│ ├── activity_tab.xml
│ └── fragment_sample.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-w820dp
│ └── dimens.xml
│ └── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── tompee
│ │ └── funtablayout
│ │ ├── BaseAdapter.java
│ │ ├── BubbleTabAdapter.java
│ │ ├── BubbleTabView.java
│ │ ├── FlipTabAdapter.java
│ │ ├── FlipTabView.java
│ │ ├── FunTabLayout.java
│ │ ├── IconView.java
│ │ ├── PopTabAdapter.java
│ │ ├── PopTabView.java
│ │ ├── SimpleTabAdapter.java
│ │ └── TitleView.java
│ └── res
│ └── values
│ ├── attrs.xml
│ ├── defaults.xml
│ ├── strings.xml
│ └── styles.xml
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
17 |
22 |
27 |
5 |
6 |
7 |
8 |
9 |
10 | ## Features
11 | - Efficient in large tab counts
12 | - Supports multiple tab view animations
13 |
14 | ## Getting started
15 | In your build.gradle:
16 |
17 | ```
18 | dependencies {
19 | compile 'com.tompee:funtablayout:1.1.0'
20 | }
21 | ```
22 |
23 | Define `FunTabLayout` and ViewPager in xml layout with custom attributes.
24 | ```xml
25 |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *
10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.tompee.funtablayout; 17 | 18 | import android.animation.ValueAnimator; 19 | import android.annotation.TargetApi; 20 | import android.content.Context; 21 | import android.content.res.TypedArray; 22 | import android.graphics.Bitmap; 23 | import android.graphics.Canvas; 24 | import android.graphics.Paint; 25 | import android.os.Build; 26 | import android.support.v4.view.ViewCompat; 27 | import android.support.v4.view.ViewPager; 28 | import android.support.v7.widget.LinearLayoutManager; 29 | import android.support.v7.widget.RecyclerView; 30 | import android.util.AttributeSet; 31 | import android.view.View; 32 | 33 | public class FunTabLayout extends RecyclerView { 34 | protected static final long DEFAULT_SCROLL_DURATION = 200; 35 | protected static final float DEFAULT_POSITION_THRESHOLD = 0.6f; 36 | protected static final float POSITION_THRESHOLD_ALLOWABLE = 0.001f; 37 | private static final String TAG = "FunTabLayout"; 38 | protected final Paint mIndicatorPaint; 39 | protected final LinearLayoutManager mLinearLayoutManager; 40 | protected int mTabVisibleCount; 41 | protected RecyclerOnScrollListener mRecyclerOnScrollListener; 42 | protected ViewPager mViewPager; 43 | protected BaseAdapter> mAdapter; 44 | 45 | protected int mIndicatorPosition; 46 | protected int mIndicatorOffset; 47 | protected int mScrollOffset; 48 | protected float mOldPositionOffset; 49 | protected float mPositionThreshold; 50 | protected boolean mRequestScrollToTab; 51 | protected boolean mScrollEanbled; 52 | protected float mTabPositionOffset; 53 | 54 | public FunTabLayout(Context context) { 55 | this(context, null); 56 | } 57 | 58 | public FunTabLayout(Context context, AttributeSet attrs) { 59 | this(context, attrs, 0); 60 | } 61 | 62 | public FunTabLayout(Context context, AttributeSet attrs, int defStyle) { 63 | super(context, attrs, defStyle); 64 | mIndicatorPaint = new Paint(); 65 | getAttributes(context, attrs, defStyle); 66 | mLinearLayoutManager = new LinearLayoutManager(getContext()) { 67 | @Override 68 | public boolean canScrollHorizontally() { 69 | return mScrollEanbled; 70 | } 71 | }; 72 | mLinearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); 73 | setLayoutManager(mLinearLayoutManager); 74 | addItemDecoration(new IndicatorDecoration()); 75 | setOverScrollMode(OVER_SCROLL_NEVER); 76 | } 77 | 78 | private void getAttributes(Context context, AttributeSet attrs, int defStyle) { 79 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FunTabLayout, defStyle, 0); 80 | mScrollEanbled = a.getBoolean(R.styleable.FunTabLayout_scrollEnabled, true); 81 | mTabVisibleCount = a.getInteger(R.styleable.FunTabLayout_tabVisibleCount, 0); 82 | mPositionThreshold = a.getFloat(R.styleable.FunTabLayout_positionThreshold, 83 | DEFAULT_POSITION_THRESHOLD); 84 | a.recycle(); 85 | } 86 | 87 | @Override 88 | protected void onDetachedFromWindow() { 89 | if (mRecyclerOnScrollListener != null) { 90 | removeOnScrollListener(mRecyclerOnScrollListener); 91 | mRecyclerOnScrollListener = null; 92 | } 93 | super.onDetachedFromWindow(); 94 | } 95 | 96 | /** 97 | * Sets the maximum number of visible tab 98 | * 99 | * @param count The new visible tab count 100 | */ 101 | public void setTabVisibleCount(int count) { 102 | mTabVisibleCount = count; 103 | } 104 | 105 | /** 106 | * Sets the position threshold on when to switch tabs 107 | * 108 | * @param threshold New position threshold 109 | */ 110 | public void setPositionThreshold(float threshold) { 111 | mPositionThreshold = threshold; 112 | } 113 | 114 | public void setUpWithAdapter(BaseAdapter> adapter) { 115 | if (adapter == null) { 116 | throw new IllegalArgumentException("Adapter cannot be null"); 117 | } 118 | mAdapter = adapter; 119 | mViewPager = adapter.getViewPager(); 120 | if (mViewPager.getAdapter() == null) { 121 | throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set"); 122 | } 123 | if (mTabVisibleCount == 0) { 124 | throw new IllegalArgumentException("Tab visible count cannot be 0"); 125 | } 126 | if (mAdapter instanceof SimpleTabAdapter) { 127 | mViewPager.addOnPageChangeListener(new SimpleTabOnPageChangeListener(this)); 128 | } else if (mAdapter instanceof BubbleTabAdapter) { 129 | mPositionThreshold = 1; 130 | mViewPager.addOnPageChangeListener(new BubbleTabOnPageChangeListener(this)); 131 | } else if (mAdapter instanceof PopTabAdapter) { 132 | mViewPager.addOnPageChangeListener(new SimpleTabOnPageChangeListener(this)); 133 | } else if (mAdapter instanceof FlipTabAdapter) { 134 | mPositionThreshold = 0.5f; 135 | mViewPager.addOnPageChangeListener(new FlipTabOnPageChangeListener(this)); 136 | } 137 | mAdapter.setTabVisibleCount(mTabVisibleCount); 138 | setAdapter(adapter); 139 | scrollToTab(mViewPager.getCurrentItem()); 140 | } 141 | 142 | public void setCurrentItem(int position, boolean smoothScroll) { 143 | if (mViewPager != null) { 144 | mViewPager.setCurrentItem(position, smoothScroll); 145 | scrollToTab(mViewPager.getCurrentItem()); 146 | return; 147 | } 148 | 149 | if (smoothScroll && position != mIndicatorPosition) { 150 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) { 151 | startAnimation(position); 152 | } else { 153 | scrollToTab(position); //FIXME add animation 154 | } 155 | 156 | } else { 157 | scrollToTab(position); 158 | } 159 | } 160 | 161 | @Override 162 | public void childDrawableStateChanged(View child) { 163 | super.childDrawableStateChanged(child); 164 | 165 | // force ItemDecorations to be drawn 166 | invalidate(); 167 | } 168 | 169 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 170 | protected void startAnimation(final int position) { 171 | 172 | float distance = 1; 173 | 174 | View view = mLinearLayoutManager.findViewByPosition(position); 175 | if (view != null) { 176 | float currentX = view.getX() + view.getMeasuredWidth() / 2.f; 177 | float centerX = getMeasuredWidth() / 2.f; 178 | distance = Math.abs(centerX - currentX) / view.getMeasuredWidth(); 179 | } 180 | 181 | ValueAnimator animator; 182 | if (position < mIndicatorPosition) { 183 | animator = ValueAnimator.ofFloat(distance, 0); 184 | } else { 185 | animator = ValueAnimator.ofFloat(-distance, 0); 186 | } 187 | animator.setDuration(DEFAULT_SCROLL_DURATION); 188 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 189 | @Override 190 | public void onAnimationUpdate(ValueAnimator animation) { 191 | scrollToTab(position, (float) animation.getAnimatedValue(), true); 192 | } 193 | }); 194 | animator.start(); 195 | } 196 | 197 | protected void scrollToTab(int position) { 198 | scrollToTab(position, 0, false); 199 | mAdapter.setCurrentIndicatorPosition(position); 200 | mAdapter.notifyDataSetChanged(); 201 | } 202 | 203 | protected void scrollToTab(int position, float positionOffset, boolean fitIndicator) { 204 | int scrollOffset = 0; 205 | 206 | View selectedView = mLinearLayoutManager.findViewByPosition(position); 207 | View nextView = mLinearLayoutManager.findViewByPosition(position + 1); 208 | 209 | if (selectedView != null) { 210 | int width = getMeasuredWidth(); 211 | float scroll1 = width / 2.f - selectedView.getMeasuredWidth() / 2.f; 212 | 213 | if (nextView != null) { 214 | float scroll2 = width / 2.f - nextView.getMeasuredWidth() / 2.f; 215 | 216 | float scroll = scroll1 + (selectedView.getMeasuredWidth() - scroll2); 217 | float dx = scroll * positionOffset; 218 | scrollOffset = (int) (scroll1 - dx); 219 | 220 | mScrollOffset = (int) dx; 221 | mIndicatorOffset = (int) ((scroll1 - scroll2) * positionOffset); 222 | 223 | } else { 224 | scrollOffset = (int) scroll1; 225 | mScrollOffset = 0; 226 | mIndicatorOffset = 0; 227 | } 228 | if (fitIndicator) { 229 | mScrollOffset = 0; 230 | mIndicatorOffset = 0; 231 | } 232 | 233 | if (mAdapter != null && mIndicatorPosition == position) { 234 | updateCurrentIndicatorPosition(position, positionOffset - mOldPositionOffset, 235 | positionOffset); 236 | } 237 | 238 | mIndicatorPosition = position; 239 | 240 | } else { 241 | mRequestScrollToTab = true; 242 | } 243 | 244 | stopScroll(); 245 | mLinearLayoutManager.scrollToPositionWithOffset(position, scrollOffset); 246 | 247 | if (mAdapter.getTabIndicatorHeight() > 0) { 248 | invalidate(); 249 | } 250 | 251 | mOldPositionOffset = positionOffset; 252 | } 253 | 254 | protected void updateCurrentIndicatorPosition(int position, float dx, float positionOffset) { 255 | int indicatorPosition = -1; 256 | if (dx > 0 && positionOffset >= mPositionThreshold - POSITION_THRESHOLD_ALLOWABLE) { 257 | indicatorPosition = position + 1; 258 | 259 | } else if (dx < 0 && positionOffset <= 1 - mPositionThreshold + POSITION_THRESHOLD_ALLOWABLE) { 260 | indicatorPosition = position; 261 | } 262 | if (indicatorPosition >= 0 && indicatorPosition != mAdapter.getCurrentIndicatorPosition()) { 263 | mAdapter.setCurrentIndicatorPosition(indicatorPosition); 264 | mAdapter.notifyDataSetChanged(); 265 | } 266 | } 267 | 268 | protected boolean isLayoutRtl() { 269 | return ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL; 270 | } 271 | 272 | protected static class RecyclerOnScrollListener extends OnScrollListener { 273 | 274 | protected final FunTabLayout mFunTabLayout; 275 | protected final LinearLayoutManager mLinearLayoutManager; 276 | public int mDx; 277 | 278 | public RecyclerOnScrollListener(FunTabLayout funTabLayout, 279 | LinearLayoutManager linearLayoutManager) { 280 | mFunTabLayout = funTabLayout; 281 | mLinearLayoutManager = linearLayoutManager; 282 | } 283 | 284 | @Override 285 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 286 | mDx += dx; 287 | } 288 | 289 | @Override 290 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 291 | switch (newState) { 292 | case SCROLL_STATE_IDLE: 293 | if (mDx > 0) { 294 | selectCenterTabForRightScroll(); 295 | } else { 296 | selectCenterTabForLeftScroll(); 297 | } 298 | mDx = 0; 299 | break; 300 | case SCROLL_STATE_DRAGGING: 301 | case SCROLL_STATE_SETTLING: 302 | } 303 | } 304 | 305 | protected void selectCenterTabForRightScroll() { 306 | int first = mLinearLayoutManager.findFirstVisibleItemPosition(); 307 | int last = mLinearLayoutManager.findLastVisibleItemPosition(); 308 | int center = mFunTabLayout.getMeasuredWidth() / 2; 309 | for (int position = first; position <= last; position++) { 310 | View view = mLinearLayoutManager.findViewByPosition(position); 311 | if (view.getLeft() + view.getMeasuredWidth() >= center) { 312 | mFunTabLayout.setCurrentItem(position, false); 313 | break; 314 | } 315 | } 316 | } 317 | 318 | protected void selectCenterTabForLeftScroll() { 319 | int first = mLinearLayoutManager.findFirstVisibleItemPosition(); 320 | int last = mLinearLayoutManager.findLastVisibleItemPosition(); 321 | int center = mFunTabLayout.getWidth() / 2; 322 | for (int position = last; position >= first; position--) { 323 | View view = mLinearLayoutManager.findViewByPosition(position); 324 | if (view.getLeft() <= center) { 325 | mFunTabLayout.setCurrentItem(position, false); 326 | break; 327 | } 328 | } 329 | } 330 | } 331 | 332 | private class SimpleTabOnPageChangeListener implements ViewPager.OnPageChangeListener { 333 | 334 | private final FunTabLayout mFunTabLayout; 335 | private int mScrollState; 336 | 337 | public SimpleTabOnPageChangeListener(FunTabLayout funTabLayout) { 338 | mFunTabLayout = funTabLayout; 339 | } 340 | 341 | @Override 342 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 343 | mFunTabLayout.scrollToTab(position, positionOffset, false); 344 | } 345 | 346 | @Override 347 | public void onPageScrollStateChanged(int state) { 348 | mScrollState = state; 349 | } 350 | 351 | @Override 352 | public void onPageSelected(int position) { 353 | if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { 354 | if (mFunTabLayout.mIndicatorPosition != position) { 355 | mFunTabLayout.scrollToTab(position); 356 | } 357 | } 358 | } 359 | } 360 | 361 | private class BubbleTabOnPageChangeListener implements ViewPager.OnPageChangeListener { 362 | 363 | private final FunTabLayout mFunTabLayout; 364 | private int mScrollState; 365 | 366 | public BubbleTabOnPageChangeListener(FunTabLayout funTabLayout) { 367 | mFunTabLayout = funTabLayout; 368 | } 369 | 370 | @Override 371 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 372 | mFunTabLayout.scrollToTab(position, positionOffset, false); 373 | BubbleTabView view = (BubbleTabView) mLinearLayoutManager.findViewByPosition(position); 374 | if (view != null) { 375 | view.setViewAlpha(positionOffset); 376 | } 377 | BubbleTabView nextView = (BubbleTabView) mLinearLayoutManager.findViewByPosition(position + 1); 378 | if (nextView != null) { 379 | nextView.setViewAlpha(1 - positionOffset); 380 | } 381 | mTabPositionOffset = positionOffset; 382 | } 383 | 384 | @Override 385 | public void onPageScrollStateChanged(int state) { 386 | mScrollState = state; 387 | } 388 | 389 | @Override 390 | public void onPageSelected(int position) { 391 | if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { 392 | if (mFunTabLayout.mIndicatorPosition != position) { 393 | mFunTabLayout.scrollToTab(position); 394 | } 395 | } 396 | } 397 | } 398 | 399 | private class FlipTabOnPageChangeListener implements ViewPager.OnPageChangeListener { 400 | 401 | private final FunTabLayout mFunTabLayout; 402 | private int mScrollState; 403 | 404 | public FlipTabOnPageChangeListener(FunTabLayout funTabLayout) { 405 | mFunTabLayout = funTabLayout; 406 | } 407 | 408 | @Override 409 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 410 | mFunTabLayout.scrollToTab(position, positionOffset, false); 411 | FlipTabView view = (FlipTabView) mLinearLayoutManager.findViewByPosition(position); 412 | if (view != null && positionOffset != 0) { 413 | view.setRotationY(positionOffset * 360); 414 | } 415 | FlipTabView nextView = (FlipTabView) mLinearLayoutManager.findViewByPosition(position + 1); 416 | if (nextView != null && positionOffset != 0) { 417 | nextView.setRotationY(positionOffset * 360); 418 | } 419 | mTabPositionOffset = positionOffset; 420 | } 421 | 422 | @Override 423 | public void onPageScrollStateChanged(int state) { 424 | mScrollState = state; 425 | } 426 | 427 | @Override 428 | public void onPageSelected(int position) { 429 | if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { 430 | if (mFunTabLayout.mIndicatorPosition != position) { 431 | mFunTabLayout.scrollToTab(position); 432 | } 433 | } 434 | } 435 | } 436 | 437 | public final class IndicatorDecoration extends RecyclerView.ItemDecoration { 438 | private static final float BUBBLE_ICON_SCALE_FACTOR = 0.80f; 439 | 440 | @Override 441 | public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) { 442 | View view = mLinearLayoutManager.findViewByPosition(mIndicatorPosition); 443 | if (view == null) { 444 | if (mRequestScrollToTab) { 445 | mRequestScrollToTab = false; 446 | scrollToTab(mViewPager.getCurrentItem()); 447 | } 448 | return; 449 | } 450 | mRequestScrollToTab = false; 451 | 452 | int left, right, top, bottom; 453 | if (mAdapter instanceof SimpleTabAdapter) { 454 | if (isLayoutRtl()) { 455 | left = view.getLeft() - mScrollOffset - mIndicatorOffset; 456 | right = view.getRight() - mScrollOffset + mIndicatorOffset; 457 | } else { 458 | left = view.getLeft() + mScrollOffset - mIndicatorOffset; 459 | right = view.getRight() + mScrollOffset + mIndicatorOffset; 460 | } 461 | mIndicatorPaint.setColor(mAdapter.getTabIndicatorColor()); 462 | top = getHeight() - mAdapter.getTabIndicatorHeight(); 463 | bottom = getHeight(); 464 | canvas.drawRect(left, top, right, bottom, mIndicatorPaint); 465 | } else if (mAdapter instanceof BubbleTabAdapter) { 466 | if (isLayoutRtl()) { 467 | left = view.getLeft() - mScrollOffset - mIndicatorOffset; 468 | right = view.getRight() - mScrollOffset + mIndicatorOffset; 469 | } else { 470 | left = view.getLeft() + mScrollOffset - mIndicatorOffset; 471 | right = view.getRight() + mScrollOffset + mIndicatorOffset; 472 | } 473 | top = getTop(); 474 | bottom = getBottom(); 475 | int centerX = (right + left) / 2; 476 | int centerY = (top + bottom) / 2; 477 | mIndicatorPaint.setColor(mAdapter.getTabIndicatorColor()); 478 | BubbleTabAdapter adapter = (BubbleTabAdapter) mAdapter; 479 | int maxRadius = (getHeight() / 2) - 12; 480 | if (mTabPositionOffset > 0.1 && mTabPositionOffset < 0.50) { 481 | canvas.drawCircle(centerX, centerY, maxRadius * (1 - mTabPositionOffset), 482 | mIndicatorPaint); 483 | } else if (mTabPositionOffset >= 0.50 && mTabPositionOffset < 0.99) { 484 | canvas.drawCircle(centerX, centerY, maxRadius * (mTabPositionOffset), mIndicatorPaint); 485 | } else { 486 | canvas.drawCircle(centerX, centerY, maxRadius, mIndicatorPaint); 487 | Bitmap bitmap = adapter.getBitmapIcon(mViewPager.getCurrentItem()); 488 | if (bitmap != null) { 489 | drawBubbleImageInCanvas(canvas, bitmap, maxRadius, centerX, centerY); 490 | } 491 | } 492 | } 493 | } 494 | 495 | private void drawBubbleImageInCanvas(Canvas canvas, Bitmap bitmap, int radius, int centerX, 496 | int centerY) { 497 | int location = (int) (Math.sin(45) * radius * BUBBLE_ICON_SCALE_FACTOR); 498 | int dimen = location * 2; 499 | canvas.drawBitmap(Bitmap.createScaledBitmap(bitmap, dimen, dimen, true), 500 | centerX - location, centerY - location, mIndicatorPaint); 501 | } 502 | } 503 | } -------------------------------------------------------------------------------- /library/src/main/java/com/tompee/funtablayout/IconView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 tompee 3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.tompee.funtablayout; 17 | 18 | import android.content.Context; 19 | import android.view.ViewGroup; 20 | import android.widget.ImageView; 21 | import android.widget.LinearLayout; 22 | 23 | class IconView extends ImageView { 24 | private int mIconDimension; 25 | 26 | public IconView(Context context) { 27 | super(context); 28 | LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup. 29 | LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 30 | params.setMargins(0, 0, 4, 0); 31 | setLayoutParams(params); 32 | } 33 | 34 | public void setIconDimension(int dimension) { 35 | mIconDimension = dimension; 36 | } 37 | 38 | @Override 39 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 40 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 41 | if (mIconDimension != 0) { 42 | setMeasuredDimension(mIconDimension, mIconDimension); 43 | } else { 44 | int height = getMeasuredHeight(); 45 | int width = getMeasuredWidth(); 46 | if (width < height) { 47 | //noinspection SuspiciousNameCombination 48 | setMeasuredDimension(width, width); 49 | } else { 50 | //noinspection SuspiciousNameCombination 51 | setMeasuredDimension(height, height); 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /library/src/main/java/com/tompee/funtablayout/PopTabAdapter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 tompee 3 | *4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.tompee.funtablayout;
17 |
18 | import android.content.Context;
19 | import android.content.res.ColorStateList;
20 | import android.graphics.Color;
21 | import android.graphics.drawable.Drawable;
22 | import android.support.annotation.DrawableRes;
23 | import android.support.v4.content.ContextCompat;
24 | import android.support.v4.graphics.drawable.DrawableCompat;
25 | import android.support.v4.view.ViewCompat;
26 | import android.support.v4.view.ViewPager;
27 | import android.support.v7.widget.RecyclerView;
28 | import android.view.View;
29 | import android.view.ViewGroup;
30 | import android.widget.ImageView;
31 | import android.widget.LinearLayout;
32 | import android.widget.TextView;
33 |
34 | public class PopTabAdapter extends BaseAdapter