() {
1134 | @Override
1135 | public SavedState createFromParcel(Parcel in, ClassLoader loader) {
1136 | return new SavedState(in, loader);
1137 | }
1138 |
1139 | @Override
1140 | public SavedState[] newArray(int size) {
1141 | return new SavedState[size];
1142 | }
1143 | });
1144 |
1145 | SavedState(Parcel in, ClassLoader loader) {
1146 | super(in);
1147 | if (loader == null) {
1148 | loader = getClass().getClassLoader();
1149 | }
1150 | position = in.readInt();
1151 | adapterState = in.readParcelable(loader);
1152 | this.loader = loader;
1153 | }
1154 | }
1155 |
1156 | @Override
1157 | public Parcelable onSaveInstanceState() {
1158 | Parcelable superState = super.onSaveInstanceState();
1159 | SavedState ss = new SavedState(superState);
1160 | ss.position = mCurItem;
1161 | if (mAdapter != null) {
1162 | ss.adapterState = mAdapter.saveState();
1163 | }
1164 | return ss;
1165 | }
1166 |
1167 | @Override
1168 | public void onRestoreInstanceState(Parcelable state) {
1169 | if (!(state instanceof SavedState)) {
1170 | super.onRestoreInstanceState(state);
1171 | return;
1172 | }
1173 |
1174 | SavedState ss = (SavedState) state;
1175 | super.onRestoreInstanceState(ss.getSuperState());
1176 |
1177 | if (mAdapter != null) {
1178 | mAdapter.restoreState(ss.adapterState, ss.loader);
1179 | setCurrentItemInternal(ss.position, false, true);
1180 | } else {
1181 | mRestoredCurItem = ss.position;
1182 | mRestoredAdapterState = ss.adapterState;
1183 | mRestoredClassLoader = ss.loader;
1184 | }
1185 | }
1186 |
1187 | @Override
1188 | public void addView(View child, int index, ViewGroup.LayoutParams params) {
1189 | if (!checkLayoutParams(params)) {
1190 | params = generateLayoutParams(params);
1191 | }
1192 | final LayoutParams lp = (LayoutParams) params;
1193 | lp.isDecor |= child instanceof Decor;
1194 | if (mInLayout) {
1195 | if (lp != null && lp.isDecor) {
1196 | throw new IllegalStateException("Cannot add pager decor view during layout");
1197 | }
1198 | lp.needsMeasure = true;
1199 | addViewInLayout(child, index, params);
1200 | } else {
1201 | super.addView(child, index, params);
1202 | }
1203 |
1204 | if (USE_CACHE) {
1205 | if (child.getVisibility() != GONE) {
1206 | child.setDrawingCacheEnabled(mScrollingCacheEnabled);
1207 | } else {
1208 | child.setDrawingCacheEnabled(false);
1209 | }
1210 | }
1211 | }
1212 |
1213 | @Override
1214 | public void removeView(View view) {
1215 | if (mInLayout) {
1216 | removeViewInLayout(view);
1217 | } else {
1218 | super.removeView(view);
1219 | }
1220 | }
1221 |
1222 | ItemInfo infoForChild(View child) {
1223 | for (int i = 0; i < mItems.size(); i++) {
1224 | ItemInfo ii = mItems.get(i);
1225 | if (mAdapter.isViewFromObject(child, ii.object)) {
1226 | return ii;
1227 | }
1228 | }
1229 | return null;
1230 | }
1231 |
1232 | ItemInfo infoForAnyChild(View child) {
1233 | ViewParent parent;
1234 | while ((parent = child.getParent()) != this) {
1235 | if (parent == null || !(parent instanceof View)) {
1236 | return null;
1237 | }
1238 | child = (View) parent;
1239 | }
1240 | return infoForChild(child);
1241 | }
1242 |
1243 | ItemInfo infoForPosition(int position) {
1244 | for (int i = 0; i < mItems.size(); i++) {
1245 | ItemInfo ii = mItems.get(i);
1246 | if (ii.position == position) {
1247 | return ii;
1248 | }
1249 | }
1250 | return null;
1251 | }
1252 |
1253 | @Override
1254 | protected void onAttachedToWindow() {
1255 | super.onAttachedToWindow();
1256 | mFirstLayout = true;
1257 | }
1258 |
1259 | @Override
1260 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1261 | // For simple implementation, our internal size is always 0.
1262 | // We depend on the container to specify the layout size of
1263 | // our view. We can't really know what it is since we will be
1264 | // adding and removing different arbitrary views and do not
1265 | // want the layout to change as this happens.
1266 | setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
1267 | getDefaultSize(0, heightMeasureSpec));
1268 |
1269 | final int measuredHeight = getMeasuredHeight();
1270 | final int maxGutterSize = measuredHeight / 10;
1271 | mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
1272 |
1273 | // Children are just made to fill our space.
1274 | int childWidthSize = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
1275 | int childHeightSize = measuredHeight - getPaddingTop() - getPaddingBottom();
1276 |
1277 | /*
1278 | * Make sure all children have been properly measured. Decor views first.
1279 | * Right now we cheat and make this less complicated by assuming decor
1280 | * views won't intersect. We will pin to edges based on gravity.
1281 | */
1282 | int size = getChildCount();
1283 | for (int i = 0; i < size; ++i) {
1284 | final View child = getChildAt(i);
1285 | if (child.getVisibility() != GONE) {
1286 | final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1287 | if (lp != null && lp.isDecor) {
1288 | final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1289 | final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
1290 | int widthMode = MeasureSpec.AT_MOST;
1291 | int heightMode = MeasureSpec.AT_MOST;
1292 | boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
1293 | boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
1294 |
1295 | if (consumeVertical) {
1296 | widthMode = MeasureSpec.EXACTLY;
1297 | } else if (consumeHorizontal) {
1298 | heightMode = MeasureSpec.EXACTLY;
1299 | }
1300 |
1301 | int widthSize = childWidthSize;
1302 | int heightSize = childHeightSize;
1303 | if (lp.width != LayoutParams.WRAP_CONTENT) {
1304 | widthMode = MeasureSpec.EXACTLY;
1305 | if (lp.width != LayoutParams.FILL_PARENT) {
1306 | widthSize = lp.width;
1307 | }
1308 | }
1309 | if (lp.height != LayoutParams.WRAP_CONTENT) {
1310 | heightMode = MeasureSpec.EXACTLY;
1311 | if (lp.height != LayoutParams.FILL_PARENT) {
1312 | heightSize = lp.height;
1313 | }
1314 | }
1315 | final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
1316 | final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
1317 | child.measure(widthSpec, heightSpec);
1318 |
1319 | if (consumeVertical) {
1320 | childHeightSize -= child.getMeasuredHeight();
1321 | } else if (consumeHorizontal) {
1322 | childWidthSize -= child.getMeasuredWidth();
1323 | }
1324 | }
1325 | }
1326 | }
1327 |
1328 | mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
1329 | mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
1330 |
1331 | // Make sure we have created all fragments that we need to have shown.
1332 | mInLayout = true;
1333 | populate();
1334 | mInLayout = false;
1335 |
1336 | // Page views next.
1337 | size = getChildCount();
1338 | for (int i = 0; i < size; ++i) {
1339 | final View child = getChildAt(i);
1340 | if (child.getVisibility() != GONE) {
1341 | if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
1342 | + ": " + mChildWidthMeasureSpec);
1343 |
1344 | final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1345 | if (lp == null || !lp.isDecor) {
1346 | final int heightSpec = MeasureSpec.makeMeasureSpec(
1347 | (int) (childHeightSize * lp.heightFactor), MeasureSpec.EXACTLY);
1348 | child.measure(mChildWidthMeasureSpec, heightSpec);
1349 | }
1350 | }
1351 | }
1352 | }
1353 |
1354 | @Override
1355 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1356 | super.onSizeChanged(w, h, oldw, oldh);
1357 |
1358 | // Make sure scroll position is set correctly.
1359 | if (h != oldh) {
1360 | recomputeScrollPosition(h, oldh, mPageMargin, mPageMargin);
1361 | }
1362 | }
1363 |
1364 | private void recomputeScrollPosition(int height, int oldHeight, int margin, int oldMargin) {
1365 | if (oldHeight > 0 && !mItems.isEmpty()) {
1366 | final int heightWithMargin = height - getPaddingTop() - getPaddingBottom() + margin;
1367 | final int oldHeightWithMargin = oldHeight - getPaddingTop() - getPaddingBottom()
1368 | + oldMargin;
1369 | final int ypos = getScrollY();
1370 | final float pageOffset = (float) ypos / oldHeightWithMargin;
1371 | final int newOffsetPixels = (int) (pageOffset * heightWithMargin);
1372 |
1373 | scrollTo(getScrollX(), newOffsetPixels);
1374 | if (!mScroller.isFinished()) {
1375 | // We now return to your regularly scheduled scroll, already in progress.
1376 | final int newDuration = mScroller.getDuration() - mScroller.timePassed();
1377 | ItemInfo targetInfo = infoForPosition(mCurItem);
1378 | mScroller.startScroll(0, newOffsetPixels,
1379 | 0, (int) (targetInfo.offset * height), newDuration);
1380 | }
1381 | } else {
1382 | final ItemInfo ii = infoForPosition(mCurItem);
1383 | final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
1384 | final int scrollPos = (int) (scrollOffset *
1385 | (height - getPaddingTop() - getPaddingBottom()));
1386 | if (scrollPos != getScrollY()) {
1387 | completeScroll(false);
1388 | scrollTo(getScrollX(), scrollPos);
1389 | }
1390 | }
1391 | }
1392 |
1393 | @Override
1394 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
1395 | final int count = getChildCount();
1396 | int width = r - l;
1397 | int height = b - t;
1398 | int paddingLeft = getPaddingLeft();
1399 | int paddingTop = getPaddingTop();
1400 | int paddingRight = getPaddingRight();
1401 | int paddingBottom = getPaddingBottom();
1402 | final int scrollY = getScrollY();
1403 |
1404 | int decorCount = 0;
1405 |
1406 | // First pass - decor views. We need to do this in two passes so that
1407 | // we have the proper offsets for non-decor views later.
1408 | for (int i = 0; i < count; i++) {
1409 | final View child = getChildAt(i);
1410 | if (child.getVisibility() != GONE) {
1411 | final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1412 | int childLeft = 0;
1413 | int childTop = 0;
1414 | if (lp.isDecor) {
1415 | final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1416 | final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
1417 | switch (hgrav) {
1418 | default:
1419 | childLeft = paddingLeft;
1420 | break;
1421 | case Gravity.LEFT:
1422 | childLeft = paddingLeft;
1423 | paddingLeft += child.getMeasuredWidth();
1424 | break;
1425 | case Gravity.CENTER_HORIZONTAL:
1426 | childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
1427 | paddingLeft);
1428 | break;
1429 | case Gravity.RIGHT:
1430 | childLeft = width - paddingRight - child.getMeasuredWidth();
1431 | paddingRight += child.getMeasuredWidth();
1432 | break;
1433 | }
1434 | switch (vgrav) {
1435 | default:
1436 | childTop = paddingTop;
1437 | break;
1438 | case Gravity.TOP:
1439 | childTop = paddingTop;
1440 | paddingTop += child.getMeasuredHeight();
1441 | break;
1442 | case Gravity.CENTER_VERTICAL:
1443 | childTop = Math.max((height - child.getMeasuredHeight()) / 2,
1444 | paddingTop);
1445 | break;
1446 | case Gravity.BOTTOM:
1447 | childTop = height - paddingBottom - child.getMeasuredHeight();
1448 | paddingBottom += child.getMeasuredHeight();
1449 | break;
1450 | }
1451 | childTop += scrollY;
1452 | child.layout(childLeft, childTop,
1453 | childLeft + child.getMeasuredWidth(),
1454 | childTop + child.getMeasuredHeight());
1455 | decorCount++;
1456 | }
1457 | }
1458 | }
1459 |
1460 | final int childHeight = height - paddingTop - paddingBottom;
1461 | // Page views. Do this once we have the right padding offsets from above.
1462 | for (int i = 0; i < count; i++) {
1463 | final View child = getChildAt(i);
1464 | if (child.getVisibility() != GONE) {
1465 | final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1466 | ItemInfo ii;
1467 | if (!lp.isDecor && (ii = infoForChild(child)) != null) {
1468 | int toff = (int) (childHeight * ii.offset);
1469 | int childLeft = paddingLeft;
1470 | int childTop = paddingTop + toff;
1471 | if (lp.needsMeasure) {
1472 | // This was added during layout and needs measurement.
1473 | // Do it now that we know what we're working with.
1474 | lp.needsMeasure = false;
1475 | final int widthSpec = MeasureSpec.makeMeasureSpec(
1476 | (int) (width - paddingLeft - paddingRight),
1477 | MeasureSpec.EXACTLY);
1478 | final int heightSpec = MeasureSpec.makeMeasureSpec(
1479 | (int) (childHeight * lp.heightFactor),
1480 | MeasureSpec.EXACTLY);
1481 | child.measure(widthSpec, heightSpec);
1482 | }
1483 | if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
1484 | + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
1485 | + "x" + child.getMeasuredHeight());
1486 | child.layout(childLeft, childTop,
1487 | childLeft + child.getMeasuredWidth(),
1488 | childTop + child.getMeasuredHeight());
1489 | }
1490 | }
1491 | }
1492 | mLeftPageBounds = paddingLeft;
1493 | mRightPageBounds = width - paddingRight;
1494 | mDecorChildCount = decorCount;
1495 |
1496 | if (mFirstLayout) {
1497 | scrollToItem(mCurItem, false, 0, false);
1498 | }
1499 | mFirstLayout = false;
1500 | }
1501 |
1502 | @Override
1503 | public void computeScroll() {
1504 | if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
1505 | int oldX = getScrollX();
1506 | int oldY = getScrollY();
1507 | int x = mScroller.getCurrX();
1508 | int y = mScroller.getCurrY();
1509 |
1510 | if (oldX != x || oldY != y) {
1511 | scrollTo(x, y);
1512 | if (!pageScrolled(y)) {
1513 | mScroller.abortAnimation();
1514 | scrollTo(x, 0);
1515 | }
1516 | }
1517 |
1518 | // Keep on drawing until the animation has finished.
1519 | ViewCompat.postInvalidateOnAnimation(this);
1520 | return;
1521 | }
1522 |
1523 | // Done with scroll, clean up state.
1524 | completeScroll(true);
1525 | }
1526 |
1527 | private boolean pageScrolled(int ypos) {
1528 | if (mItems.size() == 0) {
1529 | mCalledSuper = false;
1530 | onPageScrolled(0, 0, 0);
1531 | if (!mCalledSuper) {
1532 | throw new IllegalStateException(
1533 | "onPageScrolled did not call superclass implementation");
1534 | }
1535 | return false;
1536 | }
1537 | final ItemInfo ii = infoForCurrentScrollPosition();
1538 | final int height = getClientHeight();
1539 | final int heightWithMargin = height + mPageMargin;
1540 | final float marginOffset = (float) mPageMargin / height;
1541 | final int currentPage = ii.position;
1542 | final float pageOffset = (((float) ypos / height) - ii.offset) /
1543 | (ii.heightFactor + marginOffset);
1544 | final int offsetPixels = (int) (pageOffset * heightWithMargin);
1545 |
1546 | mCalledSuper = false;
1547 | onPageScrolled(currentPage, pageOffset, offsetPixels);
1548 | if (!mCalledSuper) {
1549 | throw new IllegalStateException(
1550 | "onPageScrolled did not call superclass implementation");
1551 | }
1552 | return true;
1553 | }
1554 |
1555 | /**
1556 | * This method will be invoked when the current page is scrolled, either as part
1557 | * of a programmatically initiated smooth scroll or a user initiated touch scroll.
1558 | * If you override this method you must call through to the superclass implementation
1559 | * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled
1560 | * returns.
1561 | *
1562 | * @param position Position index of the first page currently being displayed.
1563 | * Page position+1 will be visible if positionOffset is nonzero.
1564 | * @param offset Value from [0, 1) indicating the offset from the page at position.
1565 | * @param offsetPixels Value in pixels indicating the offset from position.
1566 | */
1567 | protected void onPageScrolled(int position, float offset, int offsetPixels) {
1568 | // Offset any decor views if needed - keep them on-screen at all times.
1569 | if (mDecorChildCount > 0) {
1570 | final int scrollY = getScrollY();
1571 | int paddingTop = getPaddingTop();
1572 | int paddingBottom = getPaddingBottom();
1573 | final int height = getHeight();
1574 | final int childCount = getChildCount();
1575 | for (int i = 0; i < childCount; i++) {
1576 | final View child = getChildAt(i);
1577 | final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1578 | if (!lp.isDecor) continue;
1579 |
1580 | final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
1581 | int childTop = 0;
1582 | switch (vgrav) {
1583 | default:
1584 | childTop = paddingTop;
1585 | break;
1586 | case Gravity.TOP:
1587 | childTop = paddingTop;
1588 | paddingTop += child.getHeight();
1589 | break;
1590 | case Gravity.CENTER_VERTICAL:
1591 | childTop = Math.max((height - child.getMeasuredHeight()) / 2,
1592 | paddingTop);
1593 | break;
1594 | case Gravity.BOTTOM:
1595 | childTop = height - paddingBottom - child.getMeasuredHeight();
1596 | paddingBottom += child.getMeasuredHeight();
1597 | break;
1598 | }
1599 | childTop += scrollY;
1600 |
1601 | final int childOffset = childTop - child.getTop();
1602 | if (childOffset != 0) {
1603 | child.offsetTopAndBottom(childOffset);
1604 | }
1605 | }
1606 | }
1607 |
1608 | if (mOnPageChangeListener != null) {
1609 | mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
1610 | }
1611 | if (mInternalPageChangeListener != null) {
1612 | mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
1613 | }
1614 |
1615 | if (mPageTransformer != null) {
1616 | final int scrollY = getScrollY();
1617 | final int childCount = getChildCount();
1618 | for (int i = 0; i < childCount; i++) {
1619 | final View child = getChildAt(i);
1620 | final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1621 |
1622 | if (lp.isDecor) continue;
1623 |
1624 | final float transformPos = (float) (child.getTop() - scrollY) / getClientHeight();
1625 | mPageTransformer.transformPage(child, transformPos);
1626 | }
1627 | }
1628 |
1629 | mCalledSuper = true;
1630 | }
1631 |
1632 | private void completeScroll(boolean postEvents) {
1633 | boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
1634 | if (needPopulate) {
1635 | // Done with scroll, no longer want to cache view drawing.
1636 | setScrollingCacheEnabled(false);
1637 | mScroller.abortAnimation();
1638 | int oldX = getScrollX();
1639 | int oldY = getScrollY();
1640 | int x = mScroller.getCurrX();
1641 | int y = mScroller.getCurrY();
1642 | if (oldX != x || oldY != y) {
1643 | scrollTo(x, y);
1644 | }
1645 | }
1646 | mPopulatePending = false;
1647 | for (int i = 0; i < mItems.size(); i++) {
1648 | ItemInfo ii = mItems.get(i);
1649 | if (ii.scrolling) {
1650 | needPopulate = true;
1651 | ii.scrolling = false;
1652 | }
1653 | }
1654 | if (needPopulate) {
1655 | if (postEvents) {
1656 | ViewCompat.postOnAnimation(this, mEndScrollRunnable);
1657 | } else {
1658 | mEndScrollRunnable.run();
1659 | }
1660 | }
1661 | }
1662 |
1663 | private boolean isGutterDrag(float y, float dy) {
1664 | return (y < mGutterSize && dy > 0) || (y > getHeight() - mGutterSize && dy < 0);
1665 | }
1666 |
1667 | private void enableLayers(boolean enable) {
1668 | final int childCount = getChildCount();
1669 | for (int i = 0; i < childCount; i++) {
1670 | final int layerType = enable ?
1671 | ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE;
1672 | ViewCompat.setLayerType(getChildAt(i), layerType, null);
1673 | }
1674 | }
1675 |
1676 | @Override
1677 | public boolean onInterceptTouchEvent(MotionEvent ev) {
1678 | /*
1679 | * This method JUST determines whether we want to intercept the motion.
1680 | * If we return true, onMotionEvent will be called and we do the actual
1681 | * scrolling there.
1682 | */
1683 |
1684 | final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
1685 |
1686 | // Always take care of the touch gesture being complete.
1687 | if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
1688 | // Release the drag.
1689 | if (DEBUG) Log.v(TAG, "Intercept done!");
1690 | mIsBeingDragged = false;
1691 | mIsUnableToDrag = false;
1692 | mActivePointerId = INVALID_POINTER;
1693 | if (mVelocityTracker != null) {
1694 | mVelocityTracker.recycle();
1695 | mVelocityTracker = null;
1696 | }
1697 | return false;
1698 | }
1699 |
1700 | // Nothing more to do here if we have decided whether or not we
1701 | // are dragging.
1702 | if (action != MotionEvent.ACTION_DOWN) {
1703 | if (mIsBeingDragged) {
1704 | if (DEBUG) Log.v(TAG, "Intercept returning true!");
1705 | return true;
1706 | }
1707 | if (mIsUnableToDrag) {
1708 | if (DEBUG) Log.v(TAG, "Intercept returning false!");
1709 | return false;
1710 | }
1711 | }
1712 |
1713 | switch (action) {
1714 | case MotionEvent.ACTION_MOVE: {
1715 | /*
1716 | * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
1717 | * whether the user has moved far enough from his original down touch.
1718 | */
1719 |
1720 | /*
1721 | * Locally do absolute value. mLastMotionY is set to the y value
1722 | * of the down event.
1723 | */
1724 | final int activePointerId = mActivePointerId;
1725 | if (activePointerId == INVALID_POINTER) {
1726 | // If we don't have a valid id, the touch down wasn't on content.
1727 | break;
1728 | }
1729 |
1730 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
1731 | final float y = MotionEventCompat.getY(ev, pointerIndex);
1732 | final float dy = y - mLastMotionY;
1733 | final float yDiff = Math.abs(dy);
1734 | final float x = MotionEventCompat.getX(ev, pointerIndex);
1735 | final float xDiff = Math.abs(x - mInitialMotionX);
1736 | if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
1737 |
1738 | if (dy != 0 && (allowDragDown(dy) || allowDragUp(dy)) && !canScroll(this, true, (int) dy, (int) x, (int) y)) {
1739 | if (DEBUG) Log.v(TAG, "Starting drag!");
1740 | mIsBeingDragged = true;
1741 | requestParentDisallowInterceptTouchEvent(true);
1742 | setScrollState(SCROLL_STATE_DRAGGING);
1743 | mLastMotionY = dy > 0 ? mInitialMotionY + mTouchSlop :
1744 | mInitialMotionY - mTouchSlop;
1745 | mLastMotionX = x;
1746 | setScrollingCacheEnabled(true);
1747 | } else if (xDiff > mTouchSlop) {
1748 | // The finger has moved enough in the vertical
1749 | // direction to be counted as a drag... abort
1750 | // any attempt to drag horizontally, to work correctly
1751 | // with children that have scrolling containers.
1752 | if (DEBUG) Log.v(TAG, "Starting unable to drag!");
1753 | mIsUnableToDrag = true;
1754 | }
1755 | if (mIsBeingDragged) {
1756 | // Scroll to follow the motion event
1757 | if (performDrag(y)) {
1758 | ViewCompat.postInvalidateOnAnimation(this);
1759 | }
1760 | }
1761 | break;
1762 | }
1763 |
1764 | case MotionEvent.ACTION_DOWN: {
1765 | /*
1766 | * Remember location of down touch.
1767 | * ACTION_DOWN always refers to pointer index 0.
1768 | */
1769 | mLastMotionX = mInitialMotionX = ev.getX();
1770 | mLastMotionY = mInitialMotionY = ev.getY();
1771 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
1772 | mIsUnableToDrag = false;
1773 |
1774 | mScroller.computeScrollOffset();
1775 | if (mScrollState == SCROLL_STATE_SETTLING &&
1776 | Math.abs(mScroller.getFinalY() - mScroller.getCurrY()) > mCloseEnough) {
1777 | // Let the user 'catch' the pager as it animates.
1778 | mScroller.abortAnimation();
1779 | mPopulatePending = false;
1780 | populate();
1781 | mIsBeingDragged = true;
1782 | requestParentDisallowInterceptTouchEvent(true);
1783 | setScrollState(SCROLL_STATE_DRAGGING);
1784 | } else {
1785 | completeScroll(false);
1786 | mIsBeingDragged = false;
1787 | }
1788 |
1789 | if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
1790 | + " mIsBeingDragged=" + mIsBeingDragged
1791 | + "mIsUnableToDrag=" + mIsUnableToDrag);
1792 | break;
1793 | }
1794 |
1795 | case MotionEventCompat.ACTION_POINTER_UP:
1796 | onSecondaryPointerUp(ev);
1797 | break;
1798 | }
1799 |
1800 | if (mVelocityTracker == null) {
1801 | mVelocityTracker = VelocityTracker.obtain();
1802 | }
1803 | mVelocityTracker.addMovement(ev);
1804 |
1805 | /*
1806 | * The only time we want to intercept motion events is if we are in the
1807 | * drag mode.
1808 | */
1809 | return mIsBeingDragged;
1810 | }
1811 |
1812 | @Override
1813 | public boolean onTouchEvent(MotionEvent ev) {
1814 | if (mFakeDragging) {
1815 | // A fake drag is in progress already, ignore this real one
1816 | // but still eat the touch events.
1817 | // (It is likely that the user is multi-touching the screen.)
1818 | return true;
1819 | }
1820 |
1821 | if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
1822 | // Don't handle edge touches immediately -- they may actually belong to one of our
1823 | // descendants.
1824 | return false;
1825 | }
1826 |
1827 | if (mAdapter == null || mAdapter.getCount() == 0) {
1828 | // Nothing to present or scroll; nothing to touch.
1829 | return false;
1830 | }
1831 |
1832 | if (mVelocityTracker == null) {
1833 | mVelocityTracker = VelocityTracker.obtain();
1834 | }
1835 | mVelocityTracker.addMovement(ev);
1836 |
1837 | final int action = ev.getAction();
1838 | boolean needsInvalidate = false;
1839 |
1840 | switch (action & MotionEventCompat.ACTION_MASK) {
1841 | case MotionEvent.ACTION_DOWN: {
1842 | mScroller.abortAnimation();
1843 | mPopulatePending = false;
1844 | populate();
1845 |
1846 | // Remember where the motion event started
1847 | mLastMotionX = mInitialMotionX = ev.getX();
1848 | mLastMotionY = mInitialMotionY = ev.getY();
1849 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
1850 | break;
1851 | }
1852 | case MotionEvent.ACTION_MOVE:
1853 | if (!mIsBeingDragged) {
1854 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
1855 | final float y = MotionEventCompat.getY(ev, pointerIndex);
1856 | final float yDiff = Math.abs(y - mLastMotionY);
1857 | final float x = MotionEventCompat.getX(ev, pointerIndex);
1858 | final float xDiff = Math.abs(x - mLastMotionX);
1859 | if (DEBUG)
1860 | Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
1861 | if (yDiff > mTouchSlop && yDiff > xDiff) {
1862 | if (DEBUG) Log.v(TAG, "Starting drag!");
1863 | mIsBeingDragged = true;
1864 | requestParentDisallowInterceptTouchEvent(true);
1865 | mLastMotionY = y - mInitialMotionY > 0 ? mInitialMotionY + mTouchSlop :
1866 | mInitialMotionY - mTouchSlop;
1867 | mLastMotionX = x;
1868 | setScrollState(SCROLL_STATE_DRAGGING);
1869 | setScrollingCacheEnabled(true);
1870 |
1871 | // Disallow Parent Intercept, just in case
1872 | ViewParent parent = getParent();
1873 | if (parent != null) {
1874 | parent.requestDisallowInterceptTouchEvent(true);
1875 | }
1876 | }
1877 | }
1878 | // Not else! Note that mIsBeingDragged can be set above.
1879 | if (mIsBeingDragged) {
1880 | // Scroll to follow the motion event
1881 | final int activePointerIndex = MotionEventCompat.findPointerIndex(
1882 | ev, mActivePointerId);
1883 | final float y = MotionEventCompat.getY(ev, activePointerIndex);
1884 | needsInvalidate |= performDrag(y);
1885 | }
1886 | break;
1887 | case MotionEvent.ACTION_UP:
1888 | if (mIsBeingDragged) {
1889 | final VelocityTracker velocityTracker = mVelocityTracker;
1890 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
1891 | int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
1892 | velocityTracker, mActivePointerId);
1893 | mPopulatePending = true;
1894 | final int height = getClientHeight();
1895 | final int scrollY = getScrollY();
1896 | final ItemInfo ii = infoForCurrentScrollPosition();
1897 | final int currentPage = ii.position;
1898 | final float pageOffset = (((float) scrollY / height) - ii.offset) / ii.heightFactor;
1899 | final int activePointerIndex =
1900 | MotionEventCompat.findPointerIndex(ev, mActivePointerId);
1901 | final float y = MotionEventCompat.getY(ev, activePointerIndex);
1902 | final int totalDelta = (int) (y - mInitialMotionY);
1903 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
1904 | totalDelta);
1905 | setCurrentItemInternal(nextPage, true, true, initialVelocity);
1906 |
1907 | mActivePointerId = INVALID_POINTER;
1908 | endDrag();
1909 | needsInvalidate = mTopEdge.onRelease() | mBottomEdge.onRelease();
1910 | }
1911 | break;
1912 | case MotionEvent.ACTION_CANCEL:
1913 | if (mIsBeingDragged) {
1914 | scrollToItem(mCurItem, true, 0, false);
1915 | mActivePointerId = INVALID_POINTER;
1916 | endDrag();
1917 | needsInvalidate = mTopEdge.onRelease() | mBottomEdge.onRelease();
1918 | }
1919 | break;
1920 | case MotionEventCompat.ACTION_POINTER_DOWN: {
1921 | final int index = MotionEventCompat.getActionIndex(ev);
1922 | final float y = MotionEventCompat.getY(ev, index);
1923 | mLastMotionY = y;
1924 | mActivePointerId = MotionEventCompat.getPointerId(ev, index);
1925 | break;
1926 | }
1927 | case MotionEventCompat.ACTION_POINTER_UP:
1928 | onSecondaryPointerUp(ev);
1929 | mLastMotionY = MotionEventCompat.getY(ev,
1930 | MotionEventCompat.findPointerIndex(ev, mActivePointerId));
1931 | break;
1932 | }
1933 | if (needsInvalidate) {
1934 | ViewCompat.postInvalidateOnAnimation(this);
1935 | }
1936 | return true;
1937 | }
1938 |
1939 | private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
1940 | final ViewParent parent = getParent();
1941 | if (parent != null) {
1942 | parent.requestDisallowInterceptTouchEvent(disallowIntercept);
1943 | }
1944 | }
1945 |
1946 | private boolean performDrag(float y) {
1947 | boolean needsInvalidate = false;
1948 |
1949 | final float deltaY = mLastMotionY - y;
1950 | mLastMotionY = y;
1951 |
1952 | float oldScrollY = getScrollY();
1953 | float scrollY = oldScrollY + deltaY;
1954 | final int height = getClientHeight();
1955 |
1956 | float topBound = height * mFirstOffset;
1957 | float bottomBound = height * mLastOffset;
1958 | boolean topAbsolute = true;
1959 | boolean bottomAbsolute = true;
1960 |
1961 | final ItemInfo firstItem = mItems.get(0);
1962 | final ItemInfo lastItem = mItems.get(mItems.size() - 1);
1963 | if (firstItem.position != 0) {
1964 | topAbsolute = false;
1965 | topBound = firstItem.offset * height;
1966 | }
1967 | if (lastItem.position != mAdapter.getCount() - 1) {
1968 | bottomAbsolute = false;
1969 | bottomBound = lastItem.offset * height;
1970 | }
1971 |
1972 | if (scrollY < topBound) {
1973 | if (topAbsolute) {
1974 | float over = topBound - scrollY;
1975 | needsInvalidate = mTopEdge.onPull(Math.abs(over) / height);
1976 | }
1977 | scrollY = topBound;
1978 | } else if (scrollY > bottomBound) {
1979 | if (bottomAbsolute) {
1980 | float over = scrollY - bottomBound;
1981 | needsInvalidate = mBottomEdge.onPull(Math.abs(over) / height);
1982 | }
1983 | scrollY = bottomBound;
1984 | }
1985 | // Don't lose the rounded component
1986 | mLastMotionX += scrollY - (int) scrollY;
1987 | scrollTo(getScrollX(), (int) scrollY);
1988 | pageScrolled((int) scrollY);
1989 |
1990 | return needsInvalidate;
1991 | }
1992 |
1993 | /**
1994 | * @return Info about the page at the current scroll position.
1995 | * This can be synthetic for a missing middle page; the 'object' field can be null.
1996 | */
1997 | private ItemInfo infoForCurrentScrollPosition() {
1998 | final int height = getClientHeight();
1999 | final float scrollOffset = height > 0 ? (float) getScrollY() / height : 0;
2000 | final float marginOffset = height > 0 ? (float) mPageMargin / height : 0;
2001 | int lastPos = -1;
2002 | float lastOffset = 0.f;
2003 | float lastHeight = 0.f;
2004 | boolean first = true;
2005 |
2006 | ItemInfo lastItem = null;
2007 | for (int i = 0; i < mItems.size(); i++) {
2008 | ItemInfo ii = mItems.get(i);
2009 | float offset;
2010 | if (!first && ii.position != lastPos + 1) {
2011 | // Create a synthetic item for a missing page.
2012 | ii = mTempItem;
2013 | ii.offset = lastOffset + lastHeight + marginOffset;
2014 | ii.position = lastPos + 1;
2015 | ii.heightFactor = mAdapter.getPageWidth(ii.position);
2016 | i--;
2017 | }
2018 | offset = ii.offset;
2019 |
2020 | final float topBound = offset;
2021 | final float bottomBound = offset + ii.heightFactor + marginOffset;
2022 | if (first || scrollOffset >= topBound) {
2023 | if (scrollOffset < bottomBound || i == mItems.size() - 1) {
2024 | return ii;
2025 | }
2026 | } else {
2027 | return lastItem;
2028 | }
2029 | first = false;
2030 | lastPos = ii.position;
2031 | lastOffset = offset;
2032 | lastHeight = ii.heightFactor;
2033 | lastItem = ii;
2034 | }
2035 |
2036 | return lastItem;
2037 | }
2038 |
2039 | private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaY) {
2040 | int targetPage;
2041 | if (Math.abs(deltaY) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
2042 | targetPage = velocity > 0 ? currentPage : currentPage + 1;
2043 | } else {
2044 | final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
2045 | targetPage = (int) (currentPage + pageOffset + truncator);
2046 | }
2047 |
2048 | if (mItems.size() > 0) {
2049 | final ItemInfo firstItem = mItems.get(0);
2050 | final ItemInfo lastItem = mItems.get(mItems.size() - 1);
2051 |
2052 | // Only let the user target pages we have items for
2053 | targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
2054 | }
2055 |
2056 | return targetPage;
2057 | }
2058 |
2059 | @Override
2060 | public void draw(Canvas canvas) {
2061 | super.draw(canvas);
2062 | boolean needsInvalidate = false;
2063 |
2064 | final int overScrollMode = ViewCompat.getOverScrollMode(this);
2065 | if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
2066 | (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
2067 | mAdapter != null && mAdapter.getCount() > 1)) {
2068 | if (!mTopEdge.isFinished()) {
2069 | final int restoreCount = canvas.save();
2070 | final int height = getHeight();
2071 | final int width = getWidth() - getPaddingLeft() - getPaddingRight();
2072 |
2073 | canvas.translate(getPaddingLeft(), mFirstOffset * height);
2074 | mTopEdge.setSize(width, height);
2075 | needsInvalidate |= mTopEdge.draw(canvas);
2076 | canvas.restoreToCount(restoreCount);
2077 | }
2078 | if (!mBottomEdge.isFinished()) {
2079 | final int restoreCount = canvas.save();
2080 | final int height = getHeight();
2081 | final int width = getWidth() - getPaddingLeft() - getPaddingRight();
2082 |
2083 | canvas.rotate(180);
2084 | canvas.translate(-width - getPaddingLeft(), -(mLastOffset + 1) * height);
2085 | mBottomEdge.setSize(width, height);
2086 | needsInvalidate |= mBottomEdge.draw(canvas);
2087 | canvas.restoreToCount(restoreCount);
2088 | }
2089 | } else {
2090 | mTopEdge.finish();
2091 | mBottomEdge.finish();
2092 | }
2093 |
2094 | if (needsInvalidate) {
2095 | // Keep animating
2096 | ViewCompat.postInvalidateOnAnimation(this);
2097 | }
2098 | }
2099 |
2100 | @Override
2101 | protected void onDraw(Canvas canvas) {
2102 | super.onDraw(canvas);
2103 |
2104 | // Draw the margin drawable between pages if needed.
2105 | if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
2106 | final int scrollY = getScrollY();
2107 | final int height = getHeight();
2108 |
2109 | final float marginOffset = (float) mPageMargin / height;
2110 | int itemIndex = 0;
2111 | ItemInfo ii = mItems.get(0);
2112 | float offset = ii.offset;
2113 | final int itemCount = mItems.size();
2114 | final int firstPos = ii.position;
2115 | final int lastPos = mItems.get(itemCount - 1).position;
2116 | for (int pos = firstPos; pos < lastPos; pos++) {
2117 | while (pos > ii.position && itemIndex < itemCount) {
2118 | ii = mItems.get(++itemIndex);
2119 | }
2120 |
2121 | float drawAt;
2122 | if (pos == ii.position) {
2123 | drawAt = (ii.offset + ii.heightFactor) * height;
2124 | offset = ii.offset + ii.heightFactor + marginOffset;
2125 | } else {
2126 | float heightFactor = mAdapter.getPageWidth(pos);
2127 | drawAt = (offset + heightFactor) * height;
2128 | offset += heightFactor + marginOffset;
2129 | }
2130 |
2131 | if (drawAt + mPageMargin > scrollY) {
2132 | mMarginDrawable.setBounds(mLeftPageBounds, (int) drawAt,
2133 | mRightPageBounds, (int) (drawAt + mPageMargin + 0.5f));
2134 | mMarginDrawable.draw(canvas);
2135 | }
2136 |
2137 | if (drawAt > scrollY + height) {
2138 | break; // No more visible, no sense in continuing
2139 | }
2140 | }
2141 | }
2142 | }
2143 |
2144 | /**
2145 | * Start a fake drag of the pager.
2146 | *
2147 | * A fake drag can be useful if you want to synchronize the motion of the ViewPager
2148 | * with the touch scrolling of another view, while still letting the ViewPager
2149 | * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.)
2150 | * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call
2151 | * {@link #endFakeDrag()} to complete the fake drag and fling as necessary.
2152 | *
2153 | * During a fake drag the ViewPager will ignore all touch events. If a real drag
2154 | * is already in progress, this method will return false.
2155 | *
2156 | * @return true if the fake drag began successfully, false if it could not be started.
2157 | * @see #fakeDragBy(float)
2158 | * @see #endFakeDrag()
2159 | */
2160 | public boolean beginFakeDrag() {
2161 | if (mIsBeingDragged) {
2162 | return false;
2163 | }
2164 | mFakeDragging = true;
2165 | setScrollState(SCROLL_STATE_DRAGGING);
2166 | mInitialMotionY = mLastMotionY = 0;
2167 | if (mVelocityTracker == null) {
2168 | mVelocityTracker = VelocityTracker.obtain();
2169 | } else {
2170 | mVelocityTracker.clear();
2171 | }
2172 | final long time = SystemClock.uptimeMillis();
2173 | final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
2174 | mVelocityTracker.addMovement(ev);
2175 | ev.recycle();
2176 | mFakeDragBeginTime = time;
2177 | return true;
2178 | }
2179 |
2180 | /**
2181 | * End a fake drag of the pager.
2182 | *
2183 | * @see #beginFakeDrag()
2184 | * @see #fakeDragBy(float)
2185 | */
2186 | public void endFakeDrag() {
2187 | if (!mFakeDragging) {
2188 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
2189 | }
2190 |
2191 | final VelocityTracker velocityTracker = mVelocityTracker;
2192 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
2193 | int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
2194 | velocityTracker, mActivePointerId);
2195 | mPopulatePending = true;
2196 | final int height = getClientHeight();
2197 | final int scrollY = getScrollY();
2198 | final ItemInfo ii = infoForCurrentScrollPosition();
2199 | final int currentPage = ii.position;
2200 | final float pageOffset = (((float) scrollY / height) - ii.offset) / ii.heightFactor;
2201 | final int totalDelta = (int) (mLastMotionY - mInitialMotionY);
2202 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
2203 | totalDelta);
2204 | setCurrentItemInternal(nextPage, true, true, initialVelocity);
2205 | endDrag();
2206 |
2207 | mFakeDragging = false;
2208 | }
2209 |
2210 | /**
2211 | * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
2212 | *
2213 | * @param yOffset Offset in pixels to drag by.
2214 | * @see #beginFakeDrag()
2215 | * @see #endFakeDrag()
2216 | */
2217 | public void fakeDragBy(float yOffset) {
2218 | if (!mFakeDragging) {
2219 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
2220 | }
2221 |
2222 | mLastMotionY += yOffset;
2223 |
2224 | float oldScrollY = getScrollY();
2225 | float scrollY = oldScrollY - yOffset;
2226 | final int height = getClientHeight();
2227 |
2228 | float topBound = height * mFirstOffset;
2229 | float bottomBound = height * mLastOffset;
2230 |
2231 | final ItemInfo firstItem = mItems.get(0);
2232 | final ItemInfo lastItem = mItems.get(mItems.size() - 1);
2233 | if (firstItem.position != 0) {
2234 | topBound = firstItem.offset * height;
2235 | }
2236 | if (lastItem.position != mAdapter.getCount() - 1) {
2237 | bottomBound = lastItem.offset * height;
2238 | }
2239 |
2240 | if (scrollY < topBound) {
2241 | scrollY = topBound;
2242 | } else if (scrollY > bottomBound) {
2243 | scrollY = bottomBound;
2244 | }
2245 | // Don't lose the rounded component
2246 | mLastMotionY += scrollY - (int) scrollY;
2247 | scrollTo(getScrollX(), (int) scrollY);
2248 | pageScrolled((int) scrollY);
2249 |
2250 | // Synthesize an event for the VelocityTracker.
2251 | final long time = SystemClock.uptimeMillis();
2252 | final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
2253 | 0, mLastMotionY, 0);
2254 | mVelocityTracker.addMovement(ev);
2255 | ev.recycle();
2256 | }
2257 |
2258 | /**
2259 | * Returns true if a fake drag is in progress.
2260 | *
2261 | * @return true if currently in a fake drag, false otherwise.
2262 | * @see #beginFakeDrag()
2263 | * @see #fakeDragBy(float)
2264 | * @see #endFakeDrag()
2265 | */
2266 | public boolean isFakeDragging() {
2267 | return mFakeDragging;
2268 | }
2269 |
2270 | private void onSecondaryPointerUp(MotionEvent ev) {
2271 | final int pointerIndex = MotionEventCompat.getActionIndex(ev);
2272 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
2273 | if (pointerId == mActivePointerId) {
2274 | // This was our active pointer going up. Choose a new
2275 | // active pointer and adjust accordingly.
2276 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
2277 | mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
2278 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
2279 | if (mVelocityTracker != null) {
2280 | mVelocityTracker.clear();
2281 | }
2282 | }
2283 | }
2284 |
2285 | private void endDrag() {
2286 | mIsBeingDragged = false;
2287 | mIsUnableToDrag = false;
2288 |
2289 | if (mVelocityTracker != null) {
2290 | mVelocityTracker.recycle();
2291 | mVelocityTracker = null;
2292 | }
2293 | }
2294 |
2295 | private void setScrollingCacheEnabled(boolean enabled) {
2296 | if (mScrollingCacheEnabled != enabled) {
2297 | mScrollingCacheEnabled = enabled;
2298 | if (USE_CACHE) {
2299 | final int size = getChildCount();
2300 | for (int i = 0; i < size; ++i) {
2301 | final View child = getChildAt(i);
2302 | if (child.getVisibility() != GONE) {
2303 | child.setDrawingCacheEnabled(enabled);
2304 | }
2305 | }
2306 | }
2307 | }
2308 | }
2309 |
2310 | public boolean internalCanScrollVertically(int direction) {
2311 | if (mAdapter == null) {
2312 | return false;
2313 | }
2314 |
2315 | final int height = getClientHeight();
2316 | final int scrollY = getScrollY();
2317 | if (direction < 0) {
2318 | return (scrollY > (int) (height * mFirstOffset));
2319 | } else if (direction > 0) {
2320 | return (scrollY < (int) (height * mLastOffset));
2321 | } else {
2322 | return false;
2323 | }
2324 | }
2325 |
2326 | /**
2327 | * Tests scrollability within child views of v given a delta of dx.
2328 | *
2329 | * @param v View to test for horizontal scrollability
2330 | * @param checkV Whether the view v passed should itself be checked for scrollability (true),
2331 | * or just its children (false).
2332 | * @param dy Delta scrolled in pixels
2333 | * @param x X coordinate of the active touch point
2334 | * @param y Y coordinate of the active touch point
2335 | * @return true if child views of v can be scrolled by delta of dx.
2336 | */
2337 | protected boolean canScroll(View v, boolean checkV, int dy, int x, int y) {
2338 | if (v instanceof ViewGroup) {
2339 | final ViewGroup group = (ViewGroup) v;
2340 | final int scrollX = v.getScrollX();
2341 | final int scrollY = v.getScrollY();
2342 | final int count = group.getChildCount();
2343 | // Count backwards - let topmost views consume scroll distance first.
2344 | for (int i = count - 1; i >= 0; i--) {
2345 | // TODO: Add versioned support here for transformed views.
2346 | // This will not work for transformed views in Honeycomb+
2347 | final View child = group.getChildAt(i);
2348 | if (y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
2349 | x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
2350 | canScroll(child, true, dy, x + scrollX - child.getLeft(),
2351 | y + scrollY - child.getTop())) {
2352 | return true;
2353 | }
2354 | }
2355 | }
2356 |
2357 | return checkV && ViewCompat.canScrollVertically(v, -dy);
2358 | }
2359 |
2360 | /**
2361 | * Return false if:
2362 | * If the adapter is in the first row
2363 | * & (If the child is a viewgroup) If the child is in the first row
2364 | * & If the scroll is trying to go up
2365 | */
2366 | protected boolean allowDragDown(float dy) {
2367 | return mCurItem != 0 && dy > 0;
2368 | }
2369 |
2370 | protected boolean allowDragUp(float dy) {
2371 | return mCurItem != getAdapter().getCount() - 1 && dy < 0;
2372 | }
2373 |
2374 | @Override
2375 | public boolean dispatchKeyEvent(KeyEvent event) {
2376 | // Let the focused view and/or our descendants get the key first
2377 | return super.dispatchKeyEvent(event) || executeKeyEvent(event);
2378 | }
2379 |
2380 | /**
2381 | * You can call this function yourself to have the scroll view perform
2382 | * scrolling from a key event, just as if the event had been dispatched to
2383 | * it by the view hierarchy.
2384 | *
2385 | * @param event The key event to execute.
2386 | * @return Return true if the event was handled, else false.
2387 | */
2388 | public boolean executeKeyEvent(KeyEvent event) {
2389 | boolean handled = false;
2390 | if (event.getAction() == KeyEvent.ACTION_DOWN) {
2391 | switch (event.getKeyCode()) {
2392 | case KeyEvent.KEYCODE_DPAD_LEFT:
2393 | handled = arrowScroll(FOCUS_LEFT);
2394 | break;
2395 | case KeyEvent.KEYCODE_DPAD_RIGHT:
2396 | handled = arrowScroll(FOCUS_RIGHT);
2397 | break;
2398 | case KeyEvent.KEYCODE_TAB:
2399 | if (Build.VERSION.SDK_INT >= 11) {
2400 | // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD
2401 | // before Android 3.0. Ignore the tab key on those devices.
2402 | if (KeyEventCompat.hasNoModifiers(event)) {
2403 | handled = arrowScroll(FOCUS_FORWARD);
2404 | } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
2405 | handled = arrowScroll(FOCUS_BACKWARD);
2406 | }
2407 | }
2408 | break;
2409 | }
2410 | }
2411 | return handled;
2412 | }
2413 |
2414 | public boolean arrowScroll(int direction) {
2415 | View currentFocused = findFocus();
2416 | if (currentFocused == this) {
2417 | currentFocused = null;
2418 | } else if (currentFocused != null) {
2419 | boolean isChild = false;
2420 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2421 | parent = parent.getParent()) {
2422 | if (parent == this) {
2423 | isChild = true;
2424 | break;
2425 | }
2426 | }
2427 | if (!isChild) {
2428 | // This would cause the focus search down below to fail in fun ways.
2429 | final StringBuilder sb = new StringBuilder();
2430 | sb.append(currentFocused.getClass().getSimpleName());
2431 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2432 | parent = parent.getParent()) {
2433 | sb.append(" => ").append(parent.getClass().getSimpleName());
2434 | }
2435 | Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
2436 | "current focused view " + sb.toString());
2437 | currentFocused = null;
2438 | }
2439 | }
2440 |
2441 | boolean handled = false;
2442 |
2443 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
2444 | direction);
2445 | if (nextFocused != null && nextFocused != currentFocused) {
2446 | if (direction == View.FOCUS_UP) {
2447 | // If there is nothing to the left, or this is causing us to
2448 | // jump to the right, then what we really want to do is page left.
2449 | final int nextTop = getChildRectInPagerCoordinates(mTempRect, nextFocused).top;
2450 | final int currTop = getChildRectInPagerCoordinates(mTempRect, currentFocused).top;
2451 | if (currentFocused != null && nextTop >= currTop) {
2452 | handled = pageUp();
2453 | } else {
2454 | handled = nextFocused.requestFocus();
2455 | }
2456 | } else if (direction == View.FOCUS_DOWN) {
2457 | // If there is nothing to the right, or this is causing us to
2458 | // jump to the left, then what we really want to do is page right.
2459 | final int nextDown = getChildRectInPagerCoordinates(mTempRect, nextFocused).bottom;
2460 | final int currDown = getChildRectInPagerCoordinates(mTempRect, currentFocused).bottom;
2461 | if (currentFocused != null && nextDown <= currDown) {
2462 | handled = pageDown();
2463 | } else {
2464 | handled = nextFocused.requestFocus();
2465 | }
2466 | }
2467 | } else if (direction == FOCUS_UP || direction == FOCUS_BACKWARD) {
2468 | // Trying to move left and nothing there; try to page.
2469 | handled = pageUp();
2470 | } else if (direction == FOCUS_DOWN || direction == FOCUS_FORWARD) {
2471 | // Trying to move right and nothing there; try to page.
2472 | handled = pageDown();
2473 | }
2474 | if (handled) {
2475 | playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
2476 | }
2477 | return handled;
2478 | }
2479 |
2480 | private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
2481 | if (outRect == null) {
2482 | outRect = new Rect();
2483 | }
2484 | if (child == null) {
2485 | outRect.set(0, 0, 0, 0);
2486 | return outRect;
2487 | }
2488 | outRect.left = child.getLeft();
2489 | outRect.right = child.getRight();
2490 | outRect.top = child.getTop();
2491 | outRect.bottom = child.getBottom();
2492 |
2493 | ViewParent parent = child.getParent();
2494 | while (parent instanceof ViewGroup && parent != this) {
2495 | final ViewGroup group = (ViewGroup) parent;
2496 | outRect.left += group.getLeft();
2497 | outRect.right += group.getRight();
2498 | outRect.top += group.getTop();
2499 | outRect.bottom += group.getBottom();
2500 |
2501 | parent = group.getParent();
2502 | }
2503 | return outRect;
2504 | }
2505 |
2506 | boolean pageUp() {
2507 | if (mCurItem > 0) {
2508 | setCurrentItem(mCurItem - 1, true);
2509 | return true;
2510 | }
2511 | return false;
2512 | }
2513 |
2514 | boolean pageDown() {
2515 | if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) {
2516 | setCurrentItem(mCurItem + 1, true);
2517 | return true;
2518 | }
2519 | return false;
2520 | }
2521 |
2522 | /**
2523 | * We only want the current page that is being shown to be focusable.
2524 | */
2525 | @Override
2526 | public void addFocusables(ArrayList views, int direction, int focusableMode) {
2527 | final int focusableCount = views.size();
2528 |
2529 | final int descendantFocusability = getDescendantFocusability();
2530 |
2531 | if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
2532 | for (int i = 0; i < getChildCount(); i++) {
2533 | final View child = getChildAt(i);
2534 | if (child.getVisibility() == VISIBLE) {
2535 | ItemInfo ii = infoForChild(child);
2536 | if (ii != null && ii.position == mCurItem) {
2537 | child.addFocusables(views, direction, focusableMode);
2538 | }
2539 | }
2540 | }
2541 | }
2542 |
2543 | // we add ourselves (if focusable) in all cases except for when we are
2544 | // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is
2545 | // to avoid the focus search finding layouts when a more precise search
2546 | // among the focusable children would be more interesting.
2547 | if (
2548 | descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
2549 | // No focusable descendants
2550 | (focusableCount == views.size())) {
2551 | // Note that we can't call the superclass here, because it will
2552 | // add all views in. So we need to do the same thing View does.
2553 | if (!isFocusable()) {
2554 | return;
2555 | }
2556 | if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE &&
2557 | isInTouchMode() && !isFocusableInTouchMode()) {
2558 | return;
2559 | }
2560 | if (views != null) {
2561 | views.add(this);
2562 | }
2563 | }
2564 | }
2565 |
2566 | /**
2567 | * We only want the current page that is being shown to be touchable.
2568 | */
2569 | @Override
2570 | public void addTouchables(ArrayList views) {
2571 | // Note that we don't call super.addTouchables(), which means that
2572 | // we don't call View.addTouchables(). This is okay because a ViewPager
2573 | // is itself not touchable.
2574 | for (int i = 0; i < getChildCount(); i++) {
2575 | final View child = getChildAt(i);
2576 | if (child.getVisibility() == VISIBLE) {
2577 | ItemInfo ii = infoForChild(child);
2578 | if (ii != null && ii.position == mCurItem) {
2579 | child.addTouchables(views);
2580 | }
2581 | }
2582 | }
2583 | }
2584 |
2585 | /**
2586 | * We only want the current page that is being shown to be focusable.
2587 | */
2588 | @Override
2589 | protected boolean onRequestFocusInDescendants(int direction,
2590 | Rect previouslyFocusedRect) {
2591 | int index;
2592 | int increment;
2593 | int end;
2594 | int count = getChildCount();
2595 | if ((direction & FOCUS_FORWARD) != 0) {
2596 | index = 0;
2597 | increment = 1;
2598 | end = count;
2599 | } else {
2600 | index = count - 1;
2601 | increment = -1;
2602 | end = -1;
2603 | }
2604 | for (int i = index; i != end; i += increment) {
2605 | View child = getChildAt(i);
2606 | if (child.getVisibility() == VISIBLE) {
2607 | ItemInfo ii = infoForChild(child);
2608 | if (ii != null && ii.position == mCurItem) {
2609 | if (child.requestFocus(direction, previouslyFocusedRect)) {
2610 | return true;
2611 | }
2612 | }
2613 | }
2614 | }
2615 | return false;
2616 | }
2617 |
2618 | @Override
2619 | public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
2620 | // Dispatch scroll events from this ViewPager.
2621 | if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) {
2622 | return super.dispatchPopulateAccessibilityEvent(event);
2623 | }
2624 |
2625 | // Dispatch all other accessibility events from the current page.
2626 | final int childCount = getChildCount();
2627 | for (int i = 0; i < childCount; i++) {
2628 | final View child = getChildAt(i);
2629 | if (child.getVisibility() == VISIBLE) {
2630 | final ItemInfo ii = infoForChild(child);
2631 | if (ii != null && ii.position == mCurItem &&
2632 | child.dispatchPopulateAccessibilityEvent(event)) {
2633 | return true;
2634 | }
2635 | }
2636 | }
2637 |
2638 | return false;
2639 | }
2640 |
2641 | @Override
2642 | protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
2643 | return new LayoutParams();
2644 | }
2645 |
2646 | @Override
2647 | protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
2648 | return generateDefaultLayoutParams();
2649 | }
2650 |
2651 | @Override
2652 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
2653 | return p instanceof LayoutParams && super.checkLayoutParams(p);
2654 | }
2655 |
2656 | @Override
2657 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
2658 | return new LayoutParams(getContext(), attrs);
2659 | }
2660 |
2661 | class MyAccessibilityDelegate extends AccessibilityDelegateCompat {
2662 |
2663 | @Override
2664 | public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
2665 | super.onInitializeAccessibilityEvent(host, event);
2666 | event.setClassName(ViewPager.class.getName());
2667 | final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain();
2668 | recordCompat.setScrollable(canScroll());
2669 | if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED
2670 | && mAdapter != null) {
2671 | recordCompat.setItemCount(mAdapter.getCount());
2672 | recordCompat.setFromIndex(mCurItem);
2673 | recordCompat.setToIndex(mCurItem);
2674 | }
2675 | }
2676 |
2677 | @Override
2678 | public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
2679 | super.onInitializeAccessibilityNodeInfo(host, info);
2680 | info.setClassName(ViewPager.class.getName());
2681 | info.setScrollable(canScroll());
2682 | if (internalCanScrollVertically(1)) {
2683 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
2684 | }
2685 | if (internalCanScrollVertically(-1)) {
2686 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
2687 | }
2688 | }
2689 |
2690 | @Override
2691 | public boolean performAccessibilityAction(View host, int action, Bundle args) {
2692 | if (super.performAccessibilityAction(host, action, args)) {
2693 | return true;
2694 | }
2695 | switch (action) {
2696 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: {
2697 | if (internalCanScrollVertically(1)) {
2698 | setCurrentItem(mCurItem + 1);
2699 | return true;
2700 | }
2701 | }
2702 | return false;
2703 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {
2704 | if (internalCanScrollVertically(-1)) {
2705 | setCurrentItem(mCurItem - 1);
2706 | return true;
2707 | }
2708 | }
2709 | return false;
2710 | }
2711 | return false;
2712 | }
2713 |
2714 | private boolean canScroll() {
2715 | return (mAdapter != null) && (mAdapter.getCount() > 1);
2716 | }
2717 | }
2718 |
2719 | private class PagerObserver extends DataSetObserver {
2720 | @Override
2721 | public void onChanged() {
2722 | dataSetChanged();
2723 | }
2724 |
2725 | @Override
2726 | public void onInvalidated() {
2727 | dataSetChanged();
2728 | }
2729 | }
2730 |
2731 | /**
2732 | * Layout parameters that should be supplied for views added to a
2733 | * ViewPager.
2734 | */
2735 | public static class LayoutParams extends ViewGroup.LayoutParams {
2736 | /**
2737 | * true if this view is a decoration on the pager itself and not
2738 | * a view supplied by the adapter.
2739 | */
2740 | public boolean isDecor;
2741 |
2742 | /**
2743 | * Gravity setting for use on decor views only:
2744 | * Where to position the view page within the overall ViewPager
2745 | * container; constants are defined in {@link android.view.Gravity}.
2746 | */
2747 | public int gravity;
2748 |
2749 | /**
2750 | * Width as a 0-1 multiplier of the measured pager width
2751 | */
2752 | float heightFactor = 0.f;
2753 |
2754 | /**
2755 | * true if this view was added during layout and needs to be measured
2756 | * before being positioned.
2757 | */
2758 | boolean needsMeasure;
2759 |
2760 | /**
2761 | * Adapter position this view is for if !isDecor
2762 | */
2763 | int position;
2764 |
2765 | /**
2766 | * Current child index within the ViewPager that this view occupies
2767 | */
2768 | int childIndex;
2769 |
2770 | public LayoutParams() {
2771 | super(FILL_PARENT, FILL_PARENT);
2772 | }
2773 |
2774 | public LayoutParams(Context context, AttributeSet attrs) {
2775 | super(context, attrs);
2776 |
2777 | final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
2778 | gravity = a.getInteger(0, Gravity.TOP);
2779 | a.recycle();
2780 | }
2781 | }
2782 |
2783 | static class ViewPositionComparator implements Comparator {
2784 | @Override
2785 | public int compare(View lhs, View rhs) {
2786 | final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
2787 | final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
2788 | if (llp.isDecor != rlp.isDecor) {
2789 | return llp.isDecor ? 1 : -1;
2790 | }
2791 | return llp.position - rlp.position;
2792 | }
2793 | }
2794 | }
2795 |
--------------------------------------------------------------------------------
/mvn_push.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Chris Banes
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 |
17 | apply plugin: 'maven'
18 | apply plugin: 'signing'
19 |
20 | def isReleaseBuild() {
21 | return version.contains("SNAPSHOT") == false
22 | }
23 |
24 | def getReleaseRepositoryUrl() {
25 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
26 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
27 | }
28 |
29 | def getSnapshotRepositoryUrl() {
30 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
31 | : "https://oss.sonatype.org/content/repositories/snapshots/"
32 | }
33 |
34 | def getRepositoryUsername() {
35 | return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : ""
36 | }
37 |
38 | def getRepositoryPassword() {
39 | return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : ""
40 | }
41 |
42 | afterEvaluate { project ->
43 | uploadArchives {
44 | repositories {
45 | mavenDeployer {
46 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
47 |
48 | pom.artifactId = POM_ARTIFACT_ID
49 |
50 | repository(url: getReleaseRepositoryUrl()) {
51 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
52 | }
53 | snapshotRepository(url: getSnapshotRepositoryUrl()) {
54 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
55 | }
56 |
57 | pom.project {
58 | name POM_NAME
59 | packaging POM_PACKAGING
60 | description POM_DESCRIPTION
61 | url POM_URL
62 |
63 | scm {
64 | url POM_SCM_URL
65 | connection POM_SCM_CONNECTION
66 | developerConnection POM_SCM_DEV_CONNECTION
67 | }
68 |
69 | licenses {
70 | license {
71 | name POM_LICENCE_NAME
72 | url POM_LICENCE_URL
73 | distribution POM_LICENCE_DIST
74 | }
75 | }
76 |
77 | developers {
78 | developer {
79 | id POM_DEVELOPER_ID
80 | name POM_DEVELOPER_NAME
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
87 |
88 | signing {
89 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
90 | sign configurations.archives
91 | }
92 |
93 | task androidJavadocs(type: Javadoc) {
94 | source = android.sourceSets.main.allJava
95 | }
96 |
97 | task androidJavadocsJar(type: Jar) {
98 | classifier = 'javadoc'
99 | from androidJavadocs.destinationDir
100 | }
101 |
102 | task androidSourcesJar(type: Jar) {
103 | classifier = 'sources'
104 | from android.sourceSets.main.allSource
105 | }
106 |
107 | artifacts {
108 | archives androidSourcesJar
109 | archives androidJavadocsJar
110 | }
111 | }
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | repositories {
4 | mavenCentral()
5 | }
6 |
7 | dependencies {
8 | compile fileTree(dir: 'libs', include: ['*.jar'])
9 | compile 'com.android.support:support-v13:20.0.0'
10 | compile 'com.android.support:support-v4:23.4.0'
11 | compile project(':library')
12 | }
13 |
14 | android {
15 | compileSdkVersion 23
16 | buildToolsVersion "23.0.3"
17 |
18 | defaultConfig {
19 | applicationId "mobileplus.verticalviewpager"
20 | minSdkVersion 14
21 | targetSdkVersion 20
22 | versionCode 1
23 | versionName "1.0"
24 | }
25 | buildTypes {
26 | release {
27 | // runProguard false
28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
29 | }
30 | }
31 | compileOptions {
32 | sourceCompatibility JavaVersion.VERSION_1_7
33 | targetCompatibility JavaVersion.VERSION_1_7
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package fr.castorflex.android.verticalviewpager.sample;
2 |
3 | import android.app.Activity;
4 | import android.app.Fragment;
5 | import android.app.FragmentManager;
6 | import android.graphics.Color;
7 | import android.graphics.drawable.ColorDrawable;
8 | import android.os.Bundle;
9 | import android.support.v13.app.FragmentPagerAdapter;
10 | import android.support.v4.view.ViewPager;
11 | import android.util.Log;
12 | import android.view.LayoutInflater;
13 | import android.view.View;
14 | import android.view.ViewGroup;
15 | import android.widget.ArrayAdapter;
16 | import android.widget.ListView;
17 |
18 | import java.util.ArrayList;
19 | import java.util.Collections;
20 | import java.util.List;
21 |
22 | import fr.castorflex.android.verticalviewpager.VerticalViewPager;
23 | import fr.castorflex.android.verticalviewpager.sample.verticaltablayout.QTabView;
24 | import fr.castorflex.android.verticalviewpager.sample.verticaltablayout.TabAdapter;
25 | import fr.castorflex.android.verticalviewpager.sample.verticaltablayout.TabView;
26 | import fr.castorflex.android.verticalviewpager.sample.verticaltablayout.VerticalTabLayout;
27 |
28 | public class MainActivity extends Activity {
29 |
30 | private static final float MIN_SCALE = 0.75f;
31 | private static final float MIN_ALPHA = 0.75f;
32 |
33 | @Override
34 | protected void onCreate(Bundle savedInstanceState) {
35 | super.onCreate(savedInstanceState);
36 | setContentView(R.layout.activity_main);
37 | final VerticalViewPager verticalViewPager = (VerticalViewPager) findViewById(R.id.verticalviewpager);
38 | final VerticalTabLayout tablayout = (VerticalTabLayout) findViewById(R.id.tablayout);
39 | tablayout.setTabAdapter(new MyTabAdapter());
40 | tablayout.addOnTabSelectedListener(new VerticalTabLayout.OnTabSelectedListener() {
41 | @Override
42 | public void onTabSelected(TabView tab, int position) {
43 | verticalViewPager.setCurrentItem(position);
44 | }
45 |
46 | @Override
47 | public void onTabReselected(TabView tab, int position) {
48 |
49 | }
50 | });
51 | verticalViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
52 | @Override
53 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
54 |
55 | }
56 |
57 | @Override
58 | public void onPageSelected(int position) {
59 | tablayout.setTabSelected(position);
60 | }
61 |
62 | @Override
63 | public void onPageScrollStateChanged(int state) {
64 |
65 | }
66 | });
67 | verticalViewPager.setAdapter(new DummyAdapter(getFragmentManager()));
68 | verticalViewPager.setPageMargin(getResources().
69 | getDimensionPixelSize(R.dimen.pagemargin));
70 | verticalViewPager.setPageMarginDrawable(new ColorDrawable(
71 | getResources().getColor(android.R.color.holo_green_dark)));
72 |
73 | verticalViewPager.setPageTransformer(true, new ViewPager.PageTransformer() {
74 | @Override
75 | public void transformPage(View view, float position) {
76 | int pageWidth = view.getWidth();
77 | int pageHeight = view.getHeight();
78 |
79 | if (position < -1) { // [-Infinity,-1)
80 | // This page is way off-screen to the left.
81 | view.setAlpha(0);
82 |
83 | } else if (position <= 1) { // [-1,1]
84 | // Modify the default slide transition to shrink the page as well
85 | float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
86 | float vertMargin = pageHeight * (1 - scaleFactor) / 2;
87 | float horzMargin = pageWidth * (1 - scaleFactor) / 2;
88 | if (position < 0) {
89 | view.setTranslationY(vertMargin - horzMargin / 2);
90 | } else {
91 | view.setTranslationY(-vertMargin + horzMargin / 2);
92 | }
93 |
94 | // Scale the page down (between MIN_SCALE and 1)
95 | view.setScaleX(scaleFactor);
96 | view.setScaleY(scaleFactor);
97 |
98 | // Fade the page relative to its size.
99 | view.setAlpha(MIN_ALPHA +
100 | (scaleFactor - MIN_SCALE) /
101 | (1 - MIN_SCALE) * (1 - MIN_ALPHA));
102 |
103 | } else { // (1,+Infinity]
104 | // This page is way off-screen to the right.
105 | view.setAlpha(0);
106 | }
107 | }
108 | });
109 | }
110 |
111 | public class DummyAdapter extends FragmentPagerAdapter {
112 | List fragments = new ArrayList<>();
113 |
114 | public DummyAdapter(FragmentManager fm) {
115 | super(fm);
116 |
117 | for (int i = 0; i < 5; i++) {
118 | fragments.add(PlaceholderFragment.newInstance(i));
119 | }
120 | }
121 |
122 | @Override
123 | public Fragment getItem(int position) {
124 | // getItem is called to instantiate the fragment for the given page.
125 | // Return a PlaceholderFragment (defined as a static inner class below).
126 | //return PlaceholderFragment.newInstance(position + 1);
127 | return fragments.get(position);
128 | }
129 |
130 | @Override
131 | public int getCount() {
132 | // Show 3 total pages.
133 | return 5;
134 | }
135 |
136 | @Override
137 | public CharSequence getPageTitle(int position) {
138 | switch (position) {
139 | case 0:
140 | return "PAGE 0";
141 | case 1:
142 | return "PAGE 1";
143 | case 2:
144 | return "PAGE 2";
145 | case 3:
146 | return "PAGE 3";
147 | case 4:
148 | return "PAGE 4";
149 | }
150 | return null;
151 | }
152 | }
153 |
154 | /**
155 | * A placeholder fragment containing a simple view.
156 | */
157 | public static class PlaceholderFragment extends Fragment {
158 | String[] array = new String[]{"Android 1", "Android 2", "Android 3",
159 | "Android 4", "Android 5", "Android 6", "Android 7", "Android 8",
160 | "Android 9", "Android 10", "Android 11", "Android 12", "Android 13",
161 | "Android 14", "Android 15", "Android 16"};
162 |
163 | /**
164 | * The fragment argument representing the section number for this
165 | * fragment.
166 | */
167 | private static final String ARG_SECTION_NUMBER = "section_number";
168 |
169 | /**
170 | * Returns a new instance of this fragment for the given section
171 | * number.
172 | */
173 | public static PlaceholderFragment newInstance(int sectionNumber) {
174 | PlaceholderFragment fragment = new PlaceholderFragment();
175 | Bundle args = new Bundle();
176 | args.putInt(ARG_SECTION_NUMBER, sectionNumber);
177 | fragment.setArguments(args);
178 | return fragment;
179 | }
180 |
181 | public PlaceholderFragment() {
182 | }
183 |
184 | @Override
185 | public View onCreateView(LayoutInflater inflater, ViewGroup container,
186 | Bundle savedInstanceState) {
187 | final View rootView = inflater.inflate(R.layout.fragment_layout, container, false);
188 |
189 | Log.d("Debug", "creating fragment "
190 | + getArguments().getInt(ARG_SECTION_NUMBER));
191 |
192 | switch (getArguments().getInt(ARG_SECTION_NUMBER)) {
193 | case 0:
194 | break;
195 |
196 | case 1:
197 | rootView.setBackgroundColor(Color.BLACK);
198 | break;
199 |
200 | case 2:
201 | rootView.setBackgroundColor(Color.BLUE);
202 | break;
203 |
204 | case 3:
205 | rootView.setBackgroundColor(Color.GREEN);
206 | break;
207 |
208 | case 4:
209 | rootView.setBackgroundColor(Color.RED);
210 | break;
211 | }
212 | final ListView listView = (ListView) rootView.findViewById(R.id.listView);
213 | listView.setAdapter(new ArrayAdapter<>(getActivity(),
214 | R.layout.list_item, R.id.text1, array));
215 |
216 | return rootView;
217 | }
218 | }
219 |
220 | class MyTabAdapter implements TabAdapter {
221 |
222 | List titles;
223 |
224 | {
225 | titles = new ArrayList<>();
226 | Collections.addAll(titles, "Android", "IOS", "Web", "JAVA", "C++"
227 | );
228 | }
229 |
230 | @Override
231 | public int getCount() {
232 | return 5;
233 | }
234 |
235 | @Override
236 | public int getBadge(int position) {
237 | if (position == 5) return position;
238 | return 0;
239 | }
240 |
241 | @Override
242 | public QTabView.TabIcon getIcon(int position) {
243 | return null;
244 | }
245 |
246 | @Override
247 | public QTabView.TabTitle getTitle(int position) {
248 | return new QTabView.TabTitle.Builder(MainActivity.this)
249 | .setContent(titles.get(position))
250 | .setTextColor(Color.BLUE, Color.BLACK)
251 | .build();
252 | }
253 |
254 | @Override
255 | public int getBackground(int position) {
256 | return 0;
257 | }
258 | }
259 |
260 | }
261 |
--------------------------------------------------------------------------------
/sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/verticaltablayout/QTabIndicator.java:
--------------------------------------------------------------------------------
1 | package fr.castorflex.android.verticalviewpager.sample.verticaltablayout;
2 |
3 | /**
4 | * Created by chqiu on 2016/9/8.
5 | */
6 | public class QTabIndicator extends TabIndicator{
7 |
8 |
9 |
10 | public QTabIndicator(){
11 |
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/verticaltablayout/QTabView.java:
--------------------------------------------------------------------------------
1 | package fr.castorflex.android.verticalviewpager.sample.verticaltablayout;
2 |
3 | import android.content.Context;
4 | import android.graphics.drawable.GradientDrawable;
5 | import android.text.TextUtils;
6 | import android.view.Gravity;
7 | import android.view.View;
8 | import android.widget.ImageView;
9 | import android.widget.LinearLayout;
10 | import android.widget.TextView;
11 |
12 | import fr.castorflex.android.verticalviewpager.sample.R;
13 |
14 |
15 | /**
16 | * @author chqiu
17 | * Email:qstumn@163.com
18 | */
19 | public class QTabView extends TabView {
20 | private Context mContext;
21 | private ImageView mIcon;
22 | private TextView mTitle;
23 | private TextView mBadge;
24 | private int mMinHeight;
25 | private TabIcon mTabIcon;
26 | private TabTitle mTabTitle;
27 | private boolean mChecked;
28 | private LinearLayout mContainer;
29 | private GradientDrawable gd;
30 |
31 | public QTabView(Context context) {
32 | super(context);
33 | mContext = context;
34 | gd = new GradientDrawable();
35 | gd.setColor(0xFFE84E40);
36 | mMinHeight = dp2px(30);
37 | mTabIcon = new TabIcon.Builder().build();
38 | mTabTitle = new TabTitle.Builder(context).build();
39 | initView();
40 | }
41 |
42 | @Override
43 | protected int[] onCreateDrawableState(int extraSpace) {
44 | final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
45 | if (isChecked()) {
46 | mergeDrawableStates(drawableState, new int[]{android.R.attr.state_checked});
47 | }
48 | return drawableState;
49 | }
50 |
51 | private void initView() {
52 | initContainer();
53 | initIconView();
54 | initTitleView();
55 | initBadge();
56 | addView(mContainer);
57 | addView(mBadge);
58 | }
59 |
60 | private void initContainer() {
61 | mContainer = new LinearLayout(mContext);
62 | mContainer.setOrientation(LinearLayout.HORIZONTAL);
63 | mContainer.setMinimumHeight(mMinHeight);
64 | mContainer.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
65 | mContainer.setGravity(Gravity.CENTER);
66 | }
67 |
68 | private void initBadge() {
69 | mBadge = new TextView(mContext);
70 | LayoutParams params2 = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
71 | params2.gravity = Gravity.RIGHT | Gravity.TOP;
72 | params2.setMargins(0, dp2px(5), dp2px(5), 0);
73 | mBadge.setLayoutParams(params2);
74 | mBadge.setGravity(Gravity.CENTER);
75 | mBadge.setTextColor(0xFFFFFFFF);
76 | mBadge.setTextSize(9);
77 | setBadge(0);
78 | }
79 |
80 | private void initTitleView() {
81 | if (mTitle != null) mContainer.removeView(mTitle);
82 | mTitle = new TextView(mContext);
83 | LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
84 | mTitle.setLayoutParams(params);
85 | mTitle.setTextColor(mTabTitle.mColorNormal);
86 | mTitle.setTextSize(mTabTitle.mTitleTextSize);
87 | mTitle.setText(mTabTitle.mContent);
88 | mTitle.setGravity(Gravity.CENTER);
89 | mTitle.setSingleLine();
90 | mTitle.setEllipsize(TextUtils.TruncateAt.END);
91 | // mTitle.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
92 | requestContainerLayout(mTabIcon.mIconGravity);
93 | }
94 |
95 | private void initIconView() {
96 | if (mIcon != null) mContainer.removeView(mIcon);
97 | mIcon = new ImageView(mContext);
98 | LayoutParams params = new LayoutParams(mTabIcon.mIconWidth, mTabIcon.mIconHeight);
99 | mIcon.setLayoutParams(params);
100 | if (mTabIcon.mNormalIcon != 0) {
101 | mIcon.setImageResource(mTabIcon.mNormalIcon);
102 | } else {
103 | mIcon.setVisibility(View.GONE);
104 | }
105 | requestContainerLayout(mTabIcon.mIconGravity);
106 | }
107 |
108 | private void setBadgeImp(int num) {
109 | LayoutParams lp = (LayoutParams) mBadge.getLayoutParams();
110 | if (num <= 9) {
111 | lp.width = dp2px(12);
112 | lp.height = dp2px(12);
113 | gd.setShape(GradientDrawable.OVAL);
114 | mBadge.setPadding(0, 0, 0, 0);
115 | } else {
116 | lp.width = LayoutParams.WRAP_CONTENT;
117 | lp.height = LayoutParams.WRAP_CONTENT;
118 | mBadge.setPadding(dp2px(3), 0, dp2px(3), 0);
119 | gd.setShape(GradientDrawable.RECTANGLE);
120 | gd.setCornerRadius(dp2px(6));
121 | }
122 | mBadge.setLayoutParams(lp);
123 | mBadge.setBackgroundDrawable(gd);
124 | mBadge.setText(String.valueOf(num));
125 | mBadge.setVisibility(View.VISIBLE);
126 | }
127 |
128 | @Override
129 | public QTabView setBadge(int num) {
130 | if (num > 0) {
131 | setBadgeImp(num);
132 | } else {
133 | mBadge.setText("");
134 | mBadge.setVisibility(View.GONE);
135 | }
136 | return this;
137 | }
138 |
139 | public QTabView setIcon(TabIcon icon) {
140 | if (icon != null)
141 | mTabIcon = icon;
142 | initIconView();
143 | setChecked(mChecked);
144 | return this;
145 | }
146 |
147 | public QTabView setTitle(TabTitle title) {
148 | if (title != null)
149 | mTabTitle = title;
150 | initTitleView();
151 | setChecked(mChecked);
152 | return this;
153 | }
154 |
155 | public QTabView setBackground(int resId) {
156 | super.setBackgroundResource(resId);
157 | return this;
158 | }
159 |
160 | private void requestContainerLayout(int gravity) {
161 | mContainer.removeAllViews();
162 | switch (gravity) {
163 | case Gravity.LEFT:
164 | mContainer.setOrientation(LinearLayout.HORIZONTAL);
165 | if (mIcon != null) {
166 | mContainer.addView(mIcon);
167 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mIcon.getLayoutParams();
168 | lp.setMargins(0, 0, mTabIcon.mMargin, 0);
169 | mIcon.setLayoutParams(lp);
170 | }
171 | if (mTitle != null)
172 | mContainer.addView(mTitle);
173 | break;
174 | case Gravity.TOP:
175 | mContainer.setOrientation(LinearLayout.VERTICAL);
176 | if (mIcon != null) {
177 | mContainer.addView(mIcon);
178 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mIcon.getLayoutParams();
179 | lp.setMargins(0, 0, 0, mTabIcon.mMargin);
180 | mIcon.setLayoutParams(lp);
181 | }
182 | if (mTitle != null)
183 | mContainer.addView(mTitle);
184 | break;
185 | case Gravity.RIGHT:
186 | mContainer.setOrientation(LinearLayout.HORIZONTAL);
187 | if (mTitle != null)
188 | mContainer.addView(mTitle);
189 | if (mIcon != null) {
190 | mContainer.addView(mIcon);
191 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mIcon.getLayoutParams();
192 | lp.setMargins(mTabIcon.mMargin, 0, 0, 0);
193 | mIcon.setLayoutParams(lp);
194 | }
195 |
196 | break;
197 | case Gravity.BOTTOM:
198 | mContainer.setOrientation(LinearLayout.VERTICAL);
199 | if (mTitle != null)
200 | mContainer.addView(mTitle);
201 | if (mIcon != null) {
202 | mContainer.addView(mIcon);
203 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mIcon.getLayoutParams();
204 | lp.setMargins(0, mTabIcon.mMargin, 0, 0);
205 | mIcon.setLayoutParams(lp);
206 | }
207 | break;
208 | }
209 | }
210 |
211 | protected int dp2px(float dp) {
212 | final float scale = mContext.getResources().getDisplayMetrics().density;
213 | return (int) (dp * scale + 0.5f);
214 | }
215 |
216 | @Override
217 | public void setChecked(boolean checked) {
218 | mChecked = checked;
219 | refreshDrawableState();
220 | if (mChecked) {
221 | mTitle.setTextColor(mTabTitle.mColorSelected);
222 | if (mTabIcon.mSelectedIcon != 0) {
223 | mIcon.setVisibility(View.VISIBLE);
224 | mIcon.setImageResource(mTabIcon.mSelectedIcon);
225 | } else {
226 | mIcon.setVisibility(View.GONE);
227 | }
228 | } else {
229 | mTitle.setTextColor(mTabTitle.mColorNormal);
230 | if (mTabIcon.mNormalIcon != 0) {
231 | mIcon.setVisibility(View.VISIBLE);
232 | mIcon.setImageResource(mTabIcon.mNormalIcon);
233 | } else {
234 | mIcon.setVisibility(View.GONE);
235 | }
236 | }
237 | }
238 |
239 | @Override
240 | public boolean isChecked() {
241 | return mChecked;
242 | }
243 |
244 | @Override
245 | public void toggle() {
246 | setChecked(!mChecked);
247 | }
248 |
249 | public static class TabIcon {
250 | public int mSelectedIcon;
251 | public int mNormalIcon;
252 | public int mIconGravity;
253 | public int mIconWidth;
254 | public int mIconHeight;
255 | public int mMargin;
256 |
257 | private TabIcon(int mSelectedIcon, int mNormalIcon, int mIconGravity, int mIconWidth, int mIconHeight, int mMargin) {
258 | this.mSelectedIcon = mSelectedIcon;
259 | this.mNormalIcon = mNormalIcon;
260 | this.mIconGravity = mIconGravity;
261 | this.mIconWidth = mIconWidth;
262 | this.mIconHeight = mIconHeight;
263 | this.mMargin = mMargin;
264 | }
265 |
266 | public static class Builder {
267 | private int mSelectedIcon;
268 | private int mNormalIcon;
269 | private int mIconGravity;
270 | private int mIconWidth;
271 | private int mIconHeight;
272 | public int mMargin;
273 |
274 | public Builder() {
275 | mSelectedIcon = 0;
276 | mNormalIcon = 0;
277 | mIconWidth = LayoutParams.WRAP_CONTENT;
278 | mIconHeight = LayoutParams.WRAP_CONTENT;
279 | mIconGravity = Gravity.LEFT;
280 | mMargin = 0;
281 | }
282 |
283 | public Builder setIcon(int selectIconResId, int normalIconResId) {
284 | mSelectedIcon = selectIconResId;
285 | mNormalIcon = normalIconResId;
286 | return this;
287 | }
288 |
289 | public Builder setIconSize(int width, int height) {
290 | mIconWidth = width;
291 | mIconHeight = height;
292 | return this;
293 | }
294 |
295 | public Builder setIconGravity(int gravity) {
296 | if (gravity != Gravity.LEFT && gravity != Gravity.RIGHT
297 | & gravity != Gravity.TOP & gravity != Gravity.BOTTOM) {
298 | throw new IllegalStateException("iconGravity only support Gravity.LEFT " +
299 | "or Gravity.RIGHT or Gravity.TOP or Gravity.BOTTOM");
300 | }
301 | mIconGravity = gravity;
302 | return this;
303 | }
304 |
305 | public Builder setIconMargin(int margin) {
306 | mMargin = margin;
307 | return this;
308 | }
309 |
310 | public TabIcon build() {
311 | return new TabIcon(mSelectedIcon, mNormalIcon, mIconGravity, mIconWidth, mIconHeight, mMargin);
312 | }
313 | }
314 | }
315 |
316 | public static class TabTitle {
317 | public int mColorSelected;
318 | public int mColorNormal;
319 | public int mTitleTextSize;
320 | public String mContent;
321 |
322 | private TabTitle(int mColorSelected, int mColorNormal, int mTitleTextSize, String mContent) {
323 | this.mColorSelected = mColorSelected;
324 | this.mColorNormal = mColorNormal;
325 | this.mTitleTextSize = mTitleTextSize;
326 | this.mContent = mContent;
327 | }
328 |
329 | public static class Builder {
330 | private int mColorSelected;
331 | private int mColorNormal;
332 | private int mTitleTextSize;
333 | private String mContent;
334 |
335 | public Builder(Context context) {
336 | this.mColorSelected = context.getResources().getColor(R.color.colorAccent);
337 | this.mColorNormal = 0xFF757575;
338 | this.mTitleTextSize = 16;
339 | this.mContent = "title";
340 | }
341 |
342 | public Builder setTextColor(int colorSelected, int colorNormal) {
343 | mColorSelected = colorSelected;
344 | mColorNormal = colorNormal;
345 | return this;
346 | }
347 |
348 | public Builder setTextSize(int sizeSp) {
349 | mTitleTextSize = sizeSp;
350 | return this;
351 | }
352 |
353 | public Builder setContent(String content) {
354 | mContent = content;
355 | return this;
356 | }
357 |
358 | public TabTitle build() {
359 | return new TabTitle(mColorSelected, mColorNormal, mTitleTextSize, mContent);
360 | }
361 | }
362 | }
363 | }
--------------------------------------------------------------------------------
/sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/verticaltablayout/TabAdapter.java:
--------------------------------------------------------------------------------
1 | package fr.castorflex.android.verticalviewpager.sample.verticaltablayout;
2 |
3 |
4 | /**
5 | * @author chqiu
6 | * Email:qstumn@163.com
7 | */
8 | public interface TabAdapter {
9 | int getCount();
10 |
11 | int getBadge(int position);
12 |
13 | QTabView.TabIcon getIcon(int position);
14 |
15 | QTabView.TabTitle getTitle(int position);
16 |
17 | int getBackground(int position);
18 | }
19 |
--------------------------------------------------------------------------------
/sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/verticaltablayout/TabIndicator.java:
--------------------------------------------------------------------------------
1 | package fr.castorflex.android.verticalviewpager.sample.verticaltablayout;
2 |
3 | import android.graphics.drawable.GradientDrawable;
4 |
5 | /**
6 | * Created by chqiu on 2016/9/8.
7 | */
8 | public abstract class TabIndicator extends GradientDrawable {
9 | protected int mIndicatorWidth;
10 | protected int mIndicatorGravity;
11 | protected float mIndicatorCorners;
12 |
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/verticaltablayout/TabView.java:
--------------------------------------------------------------------------------
1 | package fr.castorflex.android.verticalviewpager.sample.verticaltablayout;
2 |
3 | import android.content.Context;
4 | import android.widget.Checkable;
5 | import android.widget.FrameLayout;
6 |
7 | /**
8 | * @author chqiu
9 | * Email:qstumn@163.com
10 | */
11 | public abstract class TabView extends FrameLayout implements Checkable {
12 |
13 | public TabView(Context context) {
14 | super(context);
15 | }
16 |
17 | public abstract TabView setBadge(int num);
18 | }
19 |
--------------------------------------------------------------------------------
/sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/verticaltablayout/VerticalTabLayout.java:
--------------------------------------------------------------------------------
1 | package fr.castorflex.android.verticalviewpager.sample.verticaltablayout;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.animation.ValueAnimator;
6 | import android.content.Context;
7 | import android.content.res.TypedArray;
8 | import android.database.DataSetObserver;
9 | import android.graphics.Canvas;
10 | import android.graphics.Paint;
11 | import android.graphics.RectF;
12 | import android.support.annotation.Nullable;
13 | import android.support.v4.view.PagerAdapter;
14 | import android.support.v4.view.ViewPager;
15 | import android.util.AttributeSet;
16 | import android.view.Gravity;
17 | import android.view.View;
18 | import android.widget.LinearLayout;
19 | import android.widget.ScrollView;
20 |
21 | import java.util.ArrayList;
22 | import java.util.List;
23 |
24 | import fr.castorflex.android.verticalviewpager.sample.R;
25 |
26 | /**
27 | * @author chqiu
28 | * Email:qstumn@163.com
29 | */
30 | public class VerticalTabLayout extends ScrollView {
31 | private Context mContext;
32 | private TabStrip mTabStrip;
33 | private int mColorIndicator;
34 | private TabView mSelectedTab;
35 | private int mTabMargin;
36 | private int mIndicatorWidth;
37 | private int mIndicatorGravity;
38 | private float mIndicatorCorners;
39 | private TabIndicator mIndicator;
40 | private int mTabMode;
41 | private int mTabHeight;
42 |
43 | public static int TAB_MODE_FIXED = 10;
44 | public static int TAB_MODE_SCROLLABLE = 11;
45 |
46 | private ViewPager mViewPager;
47 | private PagerAdapter mPagerAdapter;
48 | private TabAdapter mTabAdapter;
49 |
50 | private List mTabSelectedListeners;
51 | private OnTabPageChangeListener mTabPageChangeListener;
52 | private DataSetObserver mPagerAdapterObserver;
53 |
54 | public VerticalTabLayout(Context context) {
55 | this(context, null);
56 | }
57 |
58 | public VerticalTabLayout(Context context, AttributeSet attrs) {
59 | this(context, attrs, 0);
60 | }
61 |
62 | public VerticalTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
63 | super(context, attrs, defStyleAttr);
64 | mContext = context;
65 | setFillViewport(true);
66 | mTabSelectedListeners = new ArrayList<>();
67 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VerticalTabLayout);
68 | mColorIndicator = typedArray.getColor(R.styleable.VerticalTabLayout_indicator_color,
69 | context.getResources().getColor(R.color.colorAccent));
70 | mIndicatorWidth = (int) typedArray.getDimension(R.styleable.VerticalTabLayout_indicator_width, dp2px(3));
71 | mIndicatorCorners = typedArray.getDimension(R.styleable.VerticalTabLayout_indicator_corners, 0);
72 | mIndicatorGravity = typedArray.getInteger(R.styleable.VerticalTabLayout_indicator_gravity, Gravity.LEFT);
73 | mTabMargin = (int) typedArray.getDimension(R.styleable.VerticalTabLayout_tab_margin, 0);
74 | mTabMode = typedArray.getInteger(R.styleable.VerticalTabLayout_tab_mode, TAB_MODE_FIXED);
75 | mTabHeight = (int) typedArray.getDimension(R.styleable.VerticalTabLayout_tab_height, LayoutParams.WRAP_CONTENT);
76 | typedArray.recycle();
77 | }
78 |
79 | @Override
80 | protected void onFinishInflate() {
81 | super.onFinishInflate();
82 | if (getChildCount() > 0) removeAllViews();
83 | initTabStrip();
84 | }
85 |
86 | private void initTabStrip() {
87 | mTabStrip = new TabStrip(mContext);
88 | addView(mTabStrip, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
89 | }
90 |
91 | public void removeAllTabs() {
92 | mTabStrip.removeAllViews();
93 | mSelectedTab = null;
94 | }
95 |
96 | public TabView getTabAt(int position) {
97 | return (TabView) mTabStrip.getChildAt(position);
98 | }
99 |
100 | private void addTabWithMode(TabView tabView) {
101 | LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
102 | initTabWithMode(params);
103 | mTabStrip.addView(tabView, params);
104 | if (mTabStrip.indexOfChild(tabView) == 0) {
105 | tabView.setChecked(true);
106 | params = (LinearLayout.LayoutParams) tabView.getLayoutParams();
107 | params.setMargins(0, 0, 0, 0);
108 | tabView.setLayoutParams(params);
109 | mSelectedTab = tabView;
110 | }
111 | }
112 |
113 | private void initTabWithMode(LinearLayout.LayoutParams params) {
114 | if (mTabMode == TAB_MODE_FIXED) {
115 | params.height = 0;
116 | params.weight = 1.0f;
117 | params.setMargins(0, 0, 0, 0);
118 | } else if (mTabMode == TAB_MODE_SCROLLABLE) {
119 | params.height = mTabHeight;
120 | params.weight = 0f;
121 | params.setMargins(0, mTabMargin, 0, 0);
122 | }
123 | }
124 |
125 | private void scrollToTab(int position) {
126 | final TabView tabView = getTabAt(position);
127 | tabView.post(new Runnable() {
128 | @Override
129 | public void run() {
130 | int y = getScrollY();
131 | int tabTop = tabView.getTop() + tabView.getHeight() / 2 - y;
132 | int target = getHeight() / 2;
133 | if (tabTop > target) {
134 | smoothScrollBy(0, tabTop - target);
135 | } else if (tabTop < target) {
136 | smoothScrollBy(0, tabTop - target);
137 | }
138 | }
139 | });
140 | }
141 |
142 | private float mLastPositionOffset;
143 |
144 | private void scrollByTab(int position, final float positionOffset) {
145 | final TabView tabView = getTabAt(position);
146 | int y = getScrollY();
147 | int tabTop = tabView.getTop() + tabView.getHeight() / 2 - y;
148 | int target = getHeight() / 2;
149 | int nextScrollY = tabView.getHeight() + mTabMargin;
150 | if (positionOffset > 0) {
151 | float percent = positionOffset - mLastPositionOffset;
152 | if (tabTop > target) {
153 | smoothScrollBy(0, (int) (nextScrollY * percent));
154 | }
155 | }
156 | mLastPositionOffset = positionOffset;
157 | }
158 |
159 | public void addTab(TabView tabView) {
160 | if (tabView != null) {
161 | addTabWithMode(tabView);
162 | tabView.setOnClickListener(new OnClickListener() {
163 | @Override
164 | public void onClick(View view) {
165 | int position = mTabStrip.indexOfChild(view);
166 | setTabSelected(position);
167 | }
168 | });
169 | } else {
170 | throw new IllegalStateException("tabview can't be null");
171 | }
172 | }
173 |
174 | public void setTabSelected(int position) {
175 | TabView view = getTabAt(position);
176 | for (int i = 0; i < mTabSelectedListeners.size(); i++) {
177 | OnTabSelectedListener listener = mTabSelectedListeners.get(i);
178 | if (listener != null) {
179 | if (view == mSelectedTab) {
180 | listener.onTabReselected(view, position);
181 | } else {
182 | listener.onTabSelected(view, position);
183 | }
184 | }
185 | }
186 | if (view != mSelectedTab) {
187 | mSelectedTab.setChecked(false);
188 | view.setChecked(true);
189 | // if (mViewPager == null)
190 | mTabStrip.moveIndicator(position);
191 | mSelectedTab = view;
192 | scrollToTab(position);
193 | }
194 | }
195 |
196 | public void setTabBadge(int tabPosition, int badgeNum) {
197 | getTabAt(tabPosition).setBadge(badgeNum);
198 | }
199 |
200 | public void setTabMode(int mode) {
201 | if (mode != TAB_MODE_FIXED && mode != TAB_MODE_SCROLLABLE) {
202 | throw new IllegalStateException("only support TAB_MODE_FIXED or TAB_MODE_SCROLLABLE");
203 | }
204 | if (mode == mTabMode) return;
205 | mTabMode = mode;
206 | for (int i = 0; i < mTabStrip.getChildCount(); i++) {
207 | View view = mTabStrip.getChildAt(i);
208 | LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams();
209 | initTabWithMode(params);
210 | if (i == 0) {
211 | params.setMargins(0, 0, 0, 0);
212 | }
213 | view.setLayoutParams(params);
214 | }
215 | mTabStrip.invalidate();
216 | mTabStrip.post(new Runnable() {
217 | @Override
218 | public void run() {
219 | mTabStrip.updataIndicatorMargin();
220 | }
221 | });
222 | }
223 |
224 | /**
225 | * only in TAB_MODE_SCROLLABLE mode will be supported
226 | *
227 | * @param margin margin
228 | */
229 | public void setTabMargin(int margin) {
230 | if (margin == mTabMargin) return;
231 | mTabMargin = margin;
232 | if (mTabMode == TAB_MODE_FIXED) return;
233 | for (int i = 0; i < mTabStrip.getChildCount(); i++) {
234 | View view = mTabStrip.getChildAt(i);
235 | LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams();
236 | params.setMargins(0, i == 0 ? 0 : mTabMargin, 0, 0);
237 | view.setLayoutParams(params);
238 | }
239 | mTabStrip.invalidate();
240 | mTabStrip.post(new Runnable() {
241 | @Override
242 | public void run() {
243 | mTabStrip.updataIndicatorMargin();
244 | }
245 | });
246 | }
247 |
248 | /**
249 | * only in TAB_MODE_SCROLLABLE mode will be supported
250 | *
251 | * @param height height
252 | */
253 | public void setTabHeight(int height) {
254 | if (height == mTabHeight) return;
255 | mTabHeight = height;
256 | if (mTabMode == TAB_MODE_FIXED) return;
257 | for (int i = 0; i < mTabStrip.getChildCount(); i++) {
258 | View view = mTabStrip.getChildAt(i);
259 | LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams();
260 | params.height = mTabHeight;
261 | view.setLayoutParams(params);
262 | }
263 | mTabStrip.invalidate();
264 | mTabStrip.post(new Runnable() {
265 | @Override
266 | public void run() {
267 | mTabStrip.updataIndicatorMargin();
268 | }
269 | });
270 | }
271 |
272 | public void setIndicatorColor(int color) {
273 | mColorIndicator = color;
274 | mTabStrip.invalidate();
275 | }
276 |
277 | public void setIndicatorWidth(int width) {
278 | mIndicatorWidth = width;
279 | mTabStrip.setIndicatorGravity();
280 | }
281 |
282 | public void setIndicatorCorners(int corners) {
283 | mIndicatorCorners = corners;
284 | mTabStrip.invalidate();
285 | }
286 |
287 | /**
288 | * @param gravity only support Gravity.LEFT,Gravity.RIGHT,Gravity.FILL
289 | */
290 | public void setIndicatorGravity(int gravity) {
291 | if (gravity == Gravity.LEFT || gravity == Gravity.RIGHT || Gravity.FILL == gravity) {
292 | mIndicatorGravity = gravity;
293 | mTabStrip.setIndicatorGravity();
294 | } else {
295 | throw new IllegalStateException("only support Gravity.LEFT,Gravity.RIGHT,Gravity.FILL");
296 | }
297 | }
298 |
299 | public void addOnTabSelectedListener(OnTabSelectedListener listener) {
300 | if (listener != null) {
301 | mTabSelectedListeners.add(listener);
302 | }
303 | }
304 |
305 | public void setTabAdapter(TabAdapter adapter) {
306 | removeAllTabs();
307 | if (adapter != null) {
308 | mTabAdapter = adapter;
309 | for (int i = 0; i < adapter.getCount(); i++) {
310 | addTab(new QTabView(mContext).setIcon(adapter.getIcon(i))
311 | .setTitle(adapter.getTitle(i)).setBadge(adapter.getBadge(i))
312 | .setBackground(adapter.getBackground(i)));
313 | }
314 | } else {
315 | removeAllTabs();
316 | }
317 | }
318 |
319 | public void setupWithViewPager(@Nullable ViewPager viewPager) {
320 | if (mViewPager != null && mTabPageChangeListener != null) {
321 | mViewPager.removeOnPageChangeListener(mTabPageChangeListener);
322 | }
323 |
324 | if (viewPager != null) {
325 | final PagerAdapter adapter = viewPager.getAdapter();
326 | if (adapter == null) {
327 | throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set");
328 | }
329 |
330 | mViewPager = viewPager;
331 |
332 | if (mTabPageChangeListener == null) {
333 | mTabPageChangeListener = new OnTabPageChangeListener();
334 | }
335 | viewPager.addOnPageChangeListener(mTabPageChangeListener);
336 |
337 | addOnTabSelectedListener(new OnTabSelectedListener() {
338 | @Override
339 | public void onTabSelected(TabView tab, int position) {
340 | if (mViewPager != null && mViewPager.getAdapter().getCount() >= position) {
341 | mViewPager.setCurrentItem(position);
342 | }
343 | }
344 |
345 | @Override
346 | public void onTabReselected(TabView tab, int position) {
347 | }
348 | });
349 |
350 | setPagerAdapter(adapter, true);
351 | } else {
352 | mViewPager = null;
353 | setPagerAdapter(null, true);
354 | }
355 | }
356 |
357 | private void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) {
358 | if (mPagerAdapter != null && mPagerAdapterObserver != null) {
359 | mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver);
360 | }
361 |
362 | mPagerAdapter = adapter;
363 |
364 | if (addObserver && adapter != null) {
365 | if (mPagerAdapterObserver == null) {
366 | mPagerAdapterObserver = new PagerAdapterObserver();
367 | }
368 | adapter.registerDataSetObserver(mPagerAdapterObserver);
369 | }
370 |
371 | populateFromPagerAdapter();
372 | }
373 |
374 | private void populateFromPagerAdapter() {
375 | removeAllTabs();
376 | if (mPagerAdapter != null) {
377 | final int adapterCount = mPagerAdapter.getCount();
378 | for (int i = 0; i < adapterCount; i++) {
379 | if (mPagerAdapter instanceof TabAdapter) {
380 | mTabAdapter = (TabAdapter) mPagerAdapter;
381 | addTab(new QTabView(mContext).setIcon(mTabAdapter.getIcon(i))
382 | .setTitle(mTabAdapter.getTitle(i)).setBadge(mTabAdapter.getBadge(i))
383 | .setBackground(mTabAdapter.getBackground(i)));
384 | } else {
385 | String title = mPagerAdapter.getPageTitle(i) == null ? "tab" + i : mPagerAdapter.getPageTitle(i).toString();
386 | addTab(new QTabView(mContext).setTitle(
387 | new QTabView.TabTitle.Builder(mContext).setContent(title).build()));
388 | }
389 | }
390 |
391 | // Make sure we reflect the currently set ViewPager item
392 | if (mViewPager != null && adapterCount > 0) {
393 | final int curItem = mViewPager.getCurrentItem();
394 | if (curItem != getSelectedTabPosition() && curItem < getTabCount()) {
395 | setTabSelected(curItem);
396 | }
397 | }
398 | } else {
399 | removeAllTabs();
400 | }
401 | }
402 |
403 | private int getTabCount() {
404 | return mTabStrip.getChildCount();
405 | }
406 |
407 | private int getSelectedTabPosition() {
408 | // if (mViewPager != null) return mViewPager.getCurrentItem();
409 | int index = mTabStrip.indexOfChild(mSelectedTab);
410 | return index == -1 ? 0 : index;
411 | }
412 |
413 |
414 | private class TabStrip extends LinearLayout {
415 | private float mIndicatorY;
416 | private float mIndicatorX;
417 | private float mIndicatorBottomY;
418 | private int mLastWidth;
419 | private int mIndicatorHeight;
420 | private Paint mIndicatorPaint;
421 | //record invalidate count,used to initialize on mIndicatorBottomY
422 | private long mInvalidateCount;
423 |
424 | public TabStrip(Context context) {
425 | super(context);
426 | setWillNotDraw(false);
427 | setOrientation(LinearLayout.VERTICAL);
428 | mIndicatorPaint = new Paint();
429 | mIndicatorGravity = mIndicatorGravity == 0 ? Gravity.LEFT : mIndicatorGravity;
430 | setIndicatorGravity();
431 | }
432 |
433 | @Override
434 | protected void onFinishInflate() {
435 | super.onFinishInflate();
436 | mIndicatorBottomY = mIndicatorHeight;
437 | }
438 |
439 | @Override
440 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
441 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
442 | if (getChildCount() > 0) {
443 | View childView = getChildAt(0);
444 | mIndicatorHeight = childView.getMeasuredHeight();
445 | if (mInvalidateCount == 0) {
446 | mIndicatorBottomY = mIndicatorHeight;
447 | }
448 | mInvalidateCount++;
449 | }
450 | }
451 |
452 | protected void updataIndicatorMargin() {
453 | int index = getSelectedTabPosition();
454 | mIndicatorY = calcIndicatorY(index);
455 | mIndicatorBottomY = mIndicatorY + mIndicatorHeight;
456 | invalidate();
457 | }
458 |
459 |
460 | protected void setIndicatorGravity() {
461 | if (mIndicatorGravity == Gravity.LEFT) {
462 | mIndicatorX = 0;
463 | if (mLastWidth != 0) mIndicatorWidth = mLastWidth;
464 | setPadding(mIndicatorWidth, 0, 0, 0);
465 | } else if (mIndicatorGravity == Gravity.RIGHT) {
466 | if (mLastWidth != 0) mIndicatorWidth = mLastWidth;
467 | setPadding(0, 0, mIndicatorWidth, 0);
468 | } else if (mIndicatorGravity == Gravity.FILL) {
469 | mIndicatorX = 0;
470 | setPadding(0, 0, 0, 0);
471 | }
472 | post(new Runnable() {
473 | @Override
474 | public void run() {
475 | if (mIndicatorGravity == Gravity.RIGHT) {
476 | mIndicatorX = getWidth() - mIndicatorWidth;
477 | } else if (mIndicatorGravity == Gravity.FILL) {
478 | mLastWidth = mIndicatorWidth;
479 | mIndicatorWidth = getWidth();
480 | }
481 | invalidate();
482 | }
483 | });
484 | }
485 |
486 | private float calcIndicatorY(float offset) {
487 | if (mTabMode == TAB_MODE_FIXED)
488 | return offset * mIndicatorHeight;
489 | return offset * (mIndicatorHeight + mTabMargin);
490 | }
491 |
492 |
493 | protected void moveIndicator(float offset) {
494 | mIndicatorY = calcIndicatorY(offset);
495 | mIndicatorBottomY = mIndicatorY + mIndicatorHeight;
496 | invalidate();
497 | }
498 |
499 | /**
500 | * move indicator to a tab location
501 | *
502 | * @param index tab location's index
503 | */
504 | protected void moveIndicator(final int index) {
505 | final int direction = index - getSelectedTabPosition();
506 | final float target = calcIndicatorY(index);
507 | final float targetBottom = target + mIndicatorHeight;
508 | if (mIndicatorY == target) return;
509 | post(new Runnable() {
510 | @Override
511 | public void run() {
512 | ValueAnimator anime = null;
513 | if (direction > 0) {
514 | anime = ValueAnimator.ofFloat(mIndicatorBottomY, targetBottom);
515 | anime.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
516 | @Override
517 | public void onAnimationUpdate(ValueAnimator animation) {
518 | float value = Float.parseFloat(animation.getAnimatedValue().toString());
519 | mIndicatorBottomY = value;
520 | invalidate();
521 | }
522 | });
523 | anime.addListener(new AnimatorListenerAdapter() {
524 | @Override
525 | public void onAnimationEnd(Animator animation) {
526 | ValueAnimator anime2 = ValueAnimator.ofFloat(mIndicatorY, target);
527 | anime2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
528 | @Override
529 | public void onAnimationUpdate(ValueAnimator animation) {
530 | float value = Float.parseFloat(animation.getAnimatedValue().toString());
531 | mIndicatorY = value;
532 | invalidate();
533 | }
534 | });
535 | anime2.setDuration(100).start();
536 | }
537 | });
538 |
539 | } else if (direction < 0) {
540 | anime = ValueAnimator.ofFloat(mIndicatorY, target);
541 | anime.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
542 | @Override
543 | public void onAnimationUpdate(ValueAnimator animation) {
544 | float value = Float.parseFloat(animation.getAnimatedValue().toString());
545 | mIndicatorY = value;
546 | invalidate();
547 | }
548 | });
549 | anime.addListener(new AnimatorListenerAdapter() {
550 | @Override
551 | public void onAnimationEnd(Animator animation) {
552 | ValueAnimator anime2 = ValueAnimator.ofFloat(mIndicatorBottomY, targetBottom);
553 | anime2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
554 | @Override
555 | public void onAnimationUpdate(ValueAnimator animation) {
556 | float value = Float.parseFloat(animation.getAnimatedValue().toString());
557 | mIndicatorBottomY = value;
558 | invalidate();
559 | }
560 | });
561 | anime2.setDuration(100).start();
562 | }
563 | });
564 | }
565 | if (anime != null) {
566 | anime.setDuration(100).start();
567 | }
568 | }
569 | });
570 | }
571 |
572 | @Override
573 | protected void onDraw(Canvas canvas) {
574 | super.onDraw(canvas);
575 | mIndicatorPaint.setColor(mColorIndicator);
576 | RectF r = new RectF(mIndicatorX, mIndicatorY,
577 | mIndicatorX + mIndicatorWidth, mIndicatorBottomY);
578 | if (mIndicatorCorners != 0) {
579 | canvas.drawRoundRect(r, mIndicatorCorners, mIndicatorCorners, mIndicatorPaint);
580 | } else {
581 | canvas.drawRect(r, mIndicatorPaint);
582 | }
583 | }
584 |
585 | }
586 |
587 | protected int dp2px(float dp) {
588 | final float scale = mContext.getResources().getDisplayMetrics().density;
589 | return (int) (dp * scale + 0.5f);
590 | }
591 |
592 | private class OnTabPageChangeListener implements ViewPager.OnPageChangeListener {
593 |
594 | public OnTabPageChangeListener() {
595 | }
596 |
597 | @Override
598 | public void onPageScrollStateChanged(int state) {
599 |
600 | }
601 |
602 | @Override
603 | public void onPageScrolled(int position, float positionOffset,
604 | int positionOffsetPixels) {
605 | // mTabStrip.moveIndicator(positionOffset + position);
606 | }
607 |
608 | @Override
609 | public void onPageSelected(int position) {
610 | if (position != getSelectedTabPosition() && !getTabAt(position).isPressed()) {
611 | setTabSelected(position);
612 | }
613 | }
614 | }
615 |
616 | private class PagerAdapterObserver extends DataSetObserver {
617 | @Override
618 | public void onChanged() {
619 | populateFromPagerAdapter();
620 | }
621 |
622 | @Override
623 | public void onInvalidated() {
624 | populateFromPagerAdapter();
625 | }
626 | }
627 |
628 | public interface OnTabSelectedListener {
629 |
630 | void onTabSelected(TabView tab, int position);
631 |
632 | void onTabReselected(TabView tab, int position);
633 | }
634 | }
635 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
14 |
25 |
26 |
31 |
32 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/fragment_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/raw/screenshot1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/raw/screenshot1.gif
--------------------------------------------------------------------------------
/sample/src/main/res/raw/screenshot2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/raw/screenshot2.gif
--------------------------------------------------------------------------------
/sample/src/main/res/raw/screenshot3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/raw/screenshot3.gif
--------------------------------------------------------------------------------
/sample/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 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | VerticalViewPager
5 |
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sample'
2 | include ':library'
3 |
--------------------------------------------------------------------------------