diff --git a/LoopingViewPager/AndroidManifest.xml b/LoopingViewPager/AndroidManifest.xml new file mode 100644 index 000000000..53181016a --- /dev/null +++ b/LoopingViewPager/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/LoopingViewPager/README.md b/LoopingViewPager/README.md new file mode 100644 index 000000000..9777790e6 --- /dev/null +++ b/LoopingViewPager/README.md @@ -0,0 +1,56 @@ +LoopingViewPager +================ + +An android ViewPager extension allowing infinite scrolling. + +This viewPager is fully compatible with [ViewPagerIndicator][1], and [PagerSlidingTabStrip][2]! + + + +Usage +----- + +To use it simply change `` to `` + +I your PagerAdapter is used only to create Views (i.e. you don't use FragmentPagerAdapter or FragmentStatePagerAdapter), +then no additional code changes are needed! + + +If you want to use LoopViewPager with FragmentPagerAdapter or FragmentStatePagerAdapter +additional changes in the adapter must be done. +The adapter must be prepared to create 2 extra items e.g.: + +The original adapter creates 4 items: `[0,1,2,3]` +The modified adapter will have to create 6 items `[0,1,2,3,4,5]` +with mapping `realPosition=(position-1)%count` +`[0->3, 1->0, 2->1, 3->2, 4->3, 5->0]` + + +Sometimes "blinking" can be seen when paginating to first or last view. +To remove this effect, simply call setBoundaryCaching( true ) on your LoopViewPager, +or change DEFAULT_BOUNDARY_CASHING to true, if you want to set boundary caching +on all LoopViewPager instances + + + +License +======= + + Copyright 2013 Leszek Mzyk + + 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. + + + + [1]: https://github.com/JakeWharton/Android-ViewPagerIndicator + [2]: https://github.com/astuetz/PagerSlidingTabStrip \ No newline at end of file diff --git a/LoopingViewPager/libs/android-support-v4.jar b/LoopingViewPager/libs/android-support-v4.jar new file mode 100644 index 000000000..cf12d2839 Binary files /dev/null and b/LoopingViewPager/libs/android-support-v4.jar differ diff --git a/LoopingViewPager/project.properties b/LoopingViewPager/project.properties new file mode 100644 index 000000000..4a46b9d1c --- /dev/null +++ b/LoopingViewPager/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +android.library=true +# Project target. +target=android-17 diff --git a/LoopingViewPager/src/com/imbryk/viewPager/LoopPagerAdapterWrapper.java b/LoopingViewPager/src/com/imbryk/viewPager/LoopPagerAdapterWrapper.java new file mode 100644 index 000000000..7b29e2b43 --- /dev/null +++ b/LoopingViewPager/src/com/imbryk/viewPager/LoopPagerAdapterWrapper.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2013 Leszek Mzyk + * + * 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.imbryk.viewPager; + +import android.os.Parcelable; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.PagerAdapter; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +/** + * A PagerAdapter wrapper responsible for providing a proper page to + * LoopViewPager + * + * This class shouldn't be used directly + */ +public class LoopPagerAdapterWrapper extends PagerAdapter { + + private PagerAdapter mAdapter; + + private SparseArray mToDestroy = new SparseArray(); + + private boolean mBoundaryCaching; + + void setBoundaryCaching(boolean flag) { + mBoundaryCaching = flag; + } + + LoopPagerAdapterWrapper(PagerAdapter adapter) { + this.mAdapter = adapter; + } + + @Override + public void notifyDataSetChanged() { + mToDestroy = new SparseArray(); + super.notifyDataSetChanged(); + } + + int toRealPosition(int position) { + int realCount = getRealCount(); + + int realPosition = (position-1) % realCount; + if (realPosition < 0) + realPosition += realCount; + + return realPosition; + } + + public int toInnerPosition(int realPosition) { + int position = (realPosition + 1); + return position; + } + + private int getRealFirstPosition() { + return 1; + } + + private int getRealLastPosition() { + return getRealFirstPosition() + getRealCount() - 1; + } + + @Override + public int getCount() { + return mAdapter.getCount() + 2; + } + + public int getRealCount() { + return mAdapter.getCount(); + } + + public PagerAdapter getRealAdapter() { + return mAdapter; + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + int realPosition = (mAdapter instanceof FragmentPagerAdapter || mAdapter instanceof FragmentStatePagerAdapter) + ? position + : toRealPosition(position); + + if (mBoundaryCaching) { + ToDestroy toDestroy = mToDestroy.get(position); + if (toDestroy != null) { + mToDestroy.remove(position); + return toDestroy.object; + } + } + return mAdapter.instantiateItem(container, realPosition); + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + int realFirst = getRealFirstPosition(); + int realLast = getRealLastPosition(); + int realPosition = (mAdapter instanceof FragmentPagerAdapter || mAdapter instanceof FragmentStatePagerAdapter) + ? position + : toRealPosition(position); + + if (mBoundaryCaching && (position == realFirst || position == realLast)) { + mToDestroy.put(position, new ToDestroy(container, realPosition, + object)); + } else { + mAdapter.destroyItem(container, realPosition, object); + } + } + + /* + * Delegate rest of methods directly to the inner adapter. + */ + + @Override + public void finishUpdate(ViewGroup container) { + mAdapter.finishUpdate(container); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return mAdapter.isViewFromObject(view, object); + } + + @Override + public void restoreState(Parcelable bundle, ClassLoader classLoader) { + mAdapter.restoreState(bundle, classLoader); + } + + @Override + public Parcelable saveState() { + return mAdapter.saveState(); + } + + @Override + public void startUpdate(ViewGroup container) { + mAdapter.startUpdate(container); + } + + /* + * End delegation + */ + + /** + * Container class for caching the boundary views + */ + static class ToDestroy { + ViewGroup container; + int position; + Object object; + + public ToDestroy(ViewGroup container, int position, Object object) { + this.container = container; + this.position = position; + this.object = object; + } + } + +} \ No newline at end of file diff --git a/LoopingViewPager/src/com/imbryk/viewPager/LoopViewPager.java b/LoopingViewPager/src/com/imbryk/viewPager/LoopViewPager.java new file mode 100644 index 000000000..2feccbc2e --- /dev/null +++ b/LoopingViewPager/src/com/imbryk/viewPager/LoopViewPager.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2013 Leszek Mzyk + * + * 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.imbryk.viewPager; + +import android.content.Context; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; + +/** + * A ViewPager subclass enabling infinte scrolling of the viewPager elements + * + * When used for paginating views (in opposite to fragments), no code changes + * should be needed only change xml's from + * to + * + * If "blinking" can be seen when paginating to first or last view, simply call + * seBoundaryCaching( true ), or change DEFAULT_BOUNDARY_CASHING to true + * + * When using a FragmentPagerAdapter or FragmentStatePagerAdapter, + * additional changes in the adapter must be done. + * The adapter must be prepared to create 2 extra items e.g.: + * + * The original adapter creates 4 items: [0,1,2,3] + * The modified adapter will have to create 6 items [0,1,2,3,4,5] + * with mapping realPosition=(position-1)%count + * [0->3, 1->0, 2->1, 3->2, 4->3, 5->0] + */ +public class LoopViewPager extends ViewPager { + + private static final boolean DEFAULT_BOUNDARY_CASHING = false; + + OnPageChangeListener mOuterPageChangeListener; + private LoopPagerAdapterWrapper mAdapter; + private boolean mBoundaryCaching = DEFAULT_BOUNDARY_CASHING; + + + /** + * helper function which may be used when implementing FragmentPagerAdapter + * + * @param position + * @param count + * @return (position-1)%count + */ + public static int toRealPosition( int position, int count ){ + position = position-1; + if( position < 0 ){ + position += count; + }else{ + position = position%count; + } + return position; + } + + /** + * If set to true, the boundary views (i.e. first and last) will never be destroyed + * This may help to prevent "blinking" of some views + * + * @param flag + */ + public void setBoundaryCaching(boolean flag) { + mBoundaryCaching = flag; + if (mAdapter != null) { + mAdapter.setBoundaryCaching(flag); + } + } + + @Override + public void setAdapter(PagerAdapter adapter) { + mAdapter = new LoopPagerAdapterWrapper(adapter); + mAdapter.setBoundaryCaching(mBoundaryCaching); + super.setAdapter(mAdapter); + setCurrentItem(0, false); + } + + @Override + public PagerAdapter getAdapter() { + return mAdapter != null ? mAdapter.getRealAdapter() : mAdapter; + } + + @Override + public int getCurrentItem() { + return mAdapter != null ? mAdapter.toRealPosition(super.getCurrentItem()) : 0; + } + + public void setCurrentItem(int item, boolean smoothScroll) { + int realItem = mAdapter.toInnerPosition(item); + super.setCurrentItem(realItem, smoothScroll); + } + + @Override + public void setCurrentItem(int item) { + if (getCurrentItem() != item) { + setCurrentItem(item, true); + } + } + + @Override + public void setOnPageChangeListener(OnPageChangeListener listener) { + mOuterPageChangeListener = listener; + }; + + public LoopViewPager(Context context) { + super(context); + init(); + } + + public LoopViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + super.setOnPageChangeListener(onPageChangeListener); + } + + private OnPageChangeListener onPageChangeListener = new OnPageChangeListener() { + private float mPreviousOffset = -1; + private float mPreviousPosition = -1; + + @Override + public void onPageSelected(int position) { + + int realPosition = mAdapter.toRealPosition(position); + if (mPreviousPosition != realPosition) { + mPreviousPosition = realPosition; + if (mOuterPageChangeListener != null) { + mOuterPageChangeListener.onPageSelected(realPosition); + } + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, + int positionOffsetPixels) { + int realPosition = position; + if (mAdapter != null) { + realPosition = mAdapter.toRealPosition(position); + + if (positionOffset == 0 + && mPreviousOffset == 0 + && (position == 0 || position == mAdapter.getCount() - 1)) { + setCurrentItem(realPosition, false); + } + } + + mPreviousOffset = positionOffset; + if (mOuterPageChangeListener != null) { + if (realPosition != mAdapter.getRealCount() - 1) { + mOuterPageChangeListener.onPageScrolled(realPosition, + positionOffset, positionOffsetPixels); + } else { + if (positionOffset > .5) { + mOuterPageChangeListener.onPageScrolled(0, 0, 0); + } else { + mOuterPageChangeListener.onPageScrolled(realPosition, + 0, 0); + } + } + } + } + + @Override + public void onPageScrollStateChanged(int state) { + if (mAdapter != null) { + int position = LoopViewPager.super.getCurrentItem(); + int realPosition = mAdapter.toRealPosition(position); + if (state == ViewPager.SCROLL_STATE_IDLE + && (position == 0 || position == mAdapter.getCount() - 1)) { + setCurrentItem(realPosition, false); + } + } + if (mOuterPageChangeListener != null) { + mOuterPageChangeListener.onPageScrollStateChanged(state); + } + } + }; + +} diff --git a/library/libs/android-support-v4.jar b/library/libs/android-support-v4.jar index 99e063b33..cf12d2839 100644 Binary files a/library/libs/android-support-v4.jar and b/library/libs/android-support-v4.jar differ diff --git a/sample/libs/android-support-v4.jar b/sample/libs/android-support-v4.jar index 99e063b33..cf12d2839 100644 Binary files a/sample/libs/android-support-v4.jar and b/sample/libs/android-support-v4.jar differ diff --git a/sample/project.properties b/sample/project.properties index 874eafdf2..198a012d9 100644 --- a/sample/project.properties +++ b/sample/project.properties @@ -10,3 +10,4 @@ # Project target. target=android-16 android.library.reference.1=../library +android.library.reference.2=../LoopingViewPager diff --git a/sample/res/layout/simple_circles.xml b/sample/res/layout/simple_circles.xml index c5da13800..d640ed42b 100644 --- a/sample/res/layout/simple_circles.xml +++ b/sample/res/layout/simple_circles.xml @@ -20,7 +20,7 @@ android:layout_width="fill_parent" android:layout_height="fill_parent"> - - - - - - - - - - -