/* * Copyright (C) 2011 Andreas Stuetz <andreas.stuetz@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.astuetz.viewpager.extensions; import java.util.ArrayList; import com.actionbarsherlock.R; import android.content.Context; import android.content.res.TypedArray; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.widget.RelativeLayout; public class SwipeyTabsView extends RelativeLayout implements OnPageChangeListener, OnTouchListener { @SuppressWarnings("unused") private static final String TAG = "com.astuetz.viewpager.extensions"; // Scrolling direction private enum Direction { None, Left, Right } private int mPosition; // This ArrayList stores the positions for each tab. private ArrayList<TabPosition> mPositions = new ArrayList<TabPosition>(); // Length of the horizontal fading edges private static final int SHADOW_WIDTH = 20; private ViewPager mPager; private TabsAdapter mAdapter; private int mTabsCount = 0; private int mWidth = 0; private int mCenter = 0; private int mHighlightOffset = 0; // store measure specs for manual re-measuring after // data has changed private int mWidthMeasureSpec = 0; private int mHeightMeasureSpec = 0; // The offset at which tabs are going to // be moved, if they are outside the screen private int mOutsideOffset = -1; private float mDragX = 0.0f; public SwipeyTabsView(Context context) { this(context, null); } public SwipeyTabsView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SwipeyTabsView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewPagerExtensions, defStyle, 0); mOutsideOffset = (int) a.getDimension(R.styleable.ViewPagerExtensions_outsideOffset, -1); a.recycle(); setHorizontalFadingEdgeEnabled(true); setFadingEdgeLength((int) (getResources().getDisplayMetrics().density * SHADOW_WIDTH)); setWillNotDraw(false); setOnTouchListener(this); } @Override protected float getLeftFadingEdgeStrength() { return 1.0f; } @Override protected float getRightFadingEdgeStrength() { return 1.0f; } /** * Notify the view that new data is available. */ public void notifyDatasetChanged() { if (mPager != null && mAdapter != null) { initTabs(); measure(mWidthMeasureSpec, mHeightMeasureSpec); calculateNewPositions(true); } } public void setAdapter(TabsAdapter adapter) { this.mAdapter = adapter; if (mPager != null && mAdapter != null) initTabs(); } /** * Binds the {@link ViewPager} to this instance * * @param pager * An instance of {@link ViewPager} */ public void setViewPager(ViewPager pager) { this.mPager = pager; mPager.setOnPageChangeListener(this); if (mPager != null && mAdapter != null) initTabs(); } /** * Initialize and add all tabs to the Layout */ private void initTabs() { // Remove all old child views removeAllViews(); mPositions.clear(); if (mAdapter == null || mPager == null) return; for (int i = 0; i < mPager.getAdapter().getCount(); i++) { addTab(mAdapter.getView(i), i); mPositions.add(new TabPosition()); } mTabsCount = getChildCount(); mPosition = mPager.getCurrentItem(); } /** * Adds a new {@link SwipeyTabButton} to the layout * * @param index * The index from the Pagers adapter * @param title * The title which should be used */ public void addTab(View tab, final int index) { if (tab == null) return; addView(tab); tab.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mPager.setCurrentItem(index); } }); tab.setOnTouchListener(this); } /** * */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { // Set a default outsideOffset if (mOutsideOffset < 0) mOutsideOffset = w; mWidth = w; mCenter = w / 2; mHighlightOffset = w / 5; if (mPager != null) mPosition = mPager.getCurrentItem(); calculateNewPositions(true); } /** * {@inheritDoc} */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int maxTabHeight = 0; mWidthMeasureSpec = widthMeasureSpec; mHeightMeasureSpec = heightMeasureSpec; final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST); final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST); for (int i = 0; i < mTabsCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) continue; child.measure(childWidthMeasureSpec, childHeightMeasureSpec); mPositions.get(i).width = child.getMeasuredWidth(); mPositions.get(i).height = child.getMeasuredHeight(); maxTabHeight = Math.max(maxTabHeight, mPositions.get(i).height); } setMeasuredDimension( resolveSize(0, widthMeasureSpec), resolveSize( maxTabHeight + getPaddingTop() + getPaddingBottom(), heightMeasureSpec)); } /** * {@inheritDoc} */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int paddingTop = getPaddingTop(); for (int i = 0; i < mTabsCount; i++) { final View tab = getChildAt(i); TabPosition pos = mPositions.get(i); if (tab instanceof SwipeyTab) { final int tabCenter = mPositions.get(i).currentPos + tab.getMeasuredWidth() / 2; final int diff = Math.abs(mCenter - tabCenter); final int p = (int) 100 * diff / mHighlightOffset; ((SwipeyTab) tab) .setHighlightPercentage(diff <= mHighlightOffset ? 100 - p : 0); } tab.layout(pos.currentPos, paddingTop, pos.currentPos + pos.width, paddingTop + pos.height); } } /** * This method calculates the previous, current and next position for each * tab * * -5 -4 -3 /-2 |-1 0 +1| +2\ +3 +4 +5 * * There are the following cases: * * [1] -5 to -3 are outside the screen * [2] -2 is outside the screen, may come into the screen when swiping right * [3] -1 is inside the screen, aligned at the left * [4] 0 is inside the screen, aligned at the center * [5] +1 is inside the screen, aligned at the right * [6] +2 is outside the screen, may come into the screen when swiping left * [7] +3 to +5 are outside the screen * * @param layout * If true, all tabs will be aligned at their initial position */ private void calculateNewPositions(boolean layout) { if (mTabsCount == 0) return; final int currentItem = mPosition; for (int i = 0; i < mTabsCount; i++) { if (i < currentItem - 2) alignLeftOutside(i, false); else if (i == currentItem - 2) alignLeftOutside(i, true); else if (i == currentItem - 1) alignLeft(i); else if (i == currentItem) alignCenter(i); else if (i == currentItem + 1) alignRight(i); else if (i == currentItem + 2) alignRightOutside(i, true); else if (i > currentItem + 2) alignRightOutside(i, false); } preventFromOverlapping(); if (layout) { for (TabPosition p : mPositions) { p.currentPos = p.oldPos; } requestLayout(); } } private int leftOutside(int position) { View tab = getChildAt(position); final int width = tab.getMeasuredWidth(); return width * (-1) - mOutsideOffset; } private int left(int position) { View tab = getChildAt(position); return 0 - tab.getPaddingLeft(); } private int center(int position) { View tab = getChildAt(position); final int width = tab.getMeasuredWidth(); return mWidth / 2 - width / 2; } private int right(int position) { View tab = getChildAt(position); final int width = tab.getMeasuredWidth(); return mWidth - width + tab.getPaddingRight(); } private int rightOutside(int position) { return mWidth + mOutsideOffset; } private void alignLeftOutside(int position, boolean canComeToLeft) { TabPosition pos = mPositions.get(position); pos.oldPos = leftOutside(position); pos.leftPos = pos.oldPos; pos.rightPos = canComeToLeft ? left(position) : pos.oldPos; } private void alignLeft(int position) { TabPosition pos = mPositions.get(position); pos.leftPos = leftOutside(position); pos.oldPos = left(position); pos.rightPos = center(position); } private void alignCenter(int position) { TabPosition pos = mPositions.get(position); pos.leftPos = left(position); pos.oldPos = center(position); pos.rightPos = right(position); } private void alignRight(int position) { TabPosition pos = mPositions.get(position); pos.leftPos = center(position); pos.oldPos = right(position); pos.rightPos = rightOutside(position); } private void alignRightOutside(int position, boolean canComeToRight) { TabPosition pos = mPositions.get(position); pos.oldPos = rightOutside(position); pos.rightPos = pos.oldPos; pos.leftPos = canComeToRight ? right(position) : pos.oldPos; } /** * */ private void preventFromOverlapping() { final int currentItem = mPosition; TabPosition leftOutside = currentItem > 1 ? mPositions .get(currentItem - 2) : null; TabPosition left = currentItem > 0 ? mPositions.get(currentItem - 1) : null; TabPosition center = mPositions.get(currentItem); TabPosition right = currentItem < mTabsCount - 1 ? mPositions .get(currentItem + 1) : null; TabPosition rightOutside = currentItem < mTabsCount - 2 ? mPositions .get(currentItem + 2) : null; if (leftOutside != null) { if (leftOutside.rightPos + leftOutside.width >= left.rightPos) { leftOutside.rightPos = left.rightPos - leftOutside.width; } } if (left != null) { if (left.oldPos + left.width >= center.oldPos) { left.oldPos = center.oldPos - left.width; } if (center.rightPos <= left.rightPos + left.width) { center.rightPos = left.rightPos + left.width; } } if (right != null) { if (right.oldPos <= center.oldPos + center.width) { right.oldPos = center.oldPos + center.width; } if (center.leftPos + center.width >= right.leftPos) { center.leftPos = right.leftPos - center.width; } } if (rightOutside != null) { if (rightOutside.leftPos <= right.leftPos + right.width) { rightOutside.leftPos = right.leftPos + right.width; } } } /** * {@inheritDoc} */ @Override public void onPageScrollStateChanged(int state) { } /** * At this point the scrolling direction is determined and every child is * interpolated to its previous or next position * * {@inheritDoc} */ @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { Direction dir = Direction.None; if (position != mPosition) { mPosition = position; calculateNewPositions(true); } final int currentScrollX = mPosition * (mPager.getWidth() + mPager.getPageMargin()); // Check if the user is swiping to the left or to the right if (mPager.getScrollX() < currentScrollX) dir = Direction.Left; else if (mPager.getScrollX() > currentScrollX) dir = Direction.Right; float x = 0.0f; if (dir == Direction.Left) x = 1 - positionOffset; else if (dir == Direction.Right) x = positionOffset; // Iterate over all tabs and set their current positions for (int i = 0; i < mTabsCount; i++) { TabPosition pos = mPositions.get(i); final float y0 = pos.oldPos; float y1 = 0.0f; if (dir == Direction.Left) y1 = pos.rightPos; else if (dir == Direction.Right) y1 = pos.leftPos; else y1 = pos.oldPos; if (y1 != y0) pos.currentPos = (int) (y0 + (y1 * x - y0 * x)); } requestLayout(); } /** * {@inheritDoc} */ @Override public void onPageSelected(int position) { } /** * Helper class which holds different positions (and the width) for a tab * */ private class TabPosition { public int oldPos; public int leftPos; public int rightPos; public int currentPos; public int width; public int height; @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("oldPos: ").append(oldPos).append(", "); sb.append("leftPos: ").append(leftPos).append(", "); sb.append("rightPos: ").append(rightPos).append(", "); sb.append("currentPos: ").append(currentPos); return sb.toString(); } } @Override public boolean onTouch(View v, MotionEvent event) { float x = event.getRawX(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mDragX = x; mPager.beginFakeDrag(); break; case MotionEvent.ACTION_MOVE: if (!mPager.isFakeDragging()) break; mPager.fakeDragBy((mDragX - x) * (-1)); mDragX = x; break; case MotionEvent.ACTION_UP: if (!mPager.isFakeDragging()) break; mPager.endFakeDrag(); break; } return v.equals(this) ? true : super.onTouchEvent(event); } }